Mid-level 6 min · March 06, 2026

TCP vs UDP — Connection Exhaustion at 5000 Players

TCP's per-connection ~3KB struct exhausted ulimit 1024 and kernel memory at 5000 players.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • TCP guarantees delivery with handshakes, ACKs, and retransmission – but adds latency and head-of-line blocking.
  • UDP is connectionless and stateless – one server socket handles unlimited senders with no overhead.
  • TCP's 20–60 byte header is larger than UDP's 8 bytes, increasing per-packet cost.
  • TCP includes flow control and congestion control; UDP leaves both to the application.
  • Production failure: a game server using TCP for 60 updates/second from 1000 players collapses under connection state overhead.
  • The real choice: if lost packets corrupt the operation, use TCP; if stale data is worse than no data, use UDP.
Plain-English First

Imagine you're sending a birthday card versus shouting across a football field. When you mail a card, the postal service confirms it arrived, resends it if it got lost, and makes sure the pages are in order — that's TCP. When you shout to your friend, you just yell and hope they hear it — no confirmation, no retry — that's UDP. One is careful and reliable; the other is fast and fire-and-forget.

Every time you load a webpage, stream a Netflix show, or jump into an online game, your computer is making a silent but critical decision: should I send this data carefully or as fast as possible? That decision comes down to two protocols — TCP and UDP — and picking the wrong one can mean the difference between a snappy app and one that feels broken. Most developers know the names but can't articulate why one exists alongside the other, which is exactly the knowledge gap that trips people up in system design interviews and production outages alike.

TCP (Transmission Control Protocol) and UDP (User Datagram Protocol) are both built on top of IP, but they solve fundamentally different problems. TCP was designed in 1974 to guarantee that data arrives completely and in order — critical for things like file transfers or HTTP requests where a missing byte corrupts everything. UDP was designed for situations where speed trumps perfection, where a lost packet is better than a late one, and where the application itself can decide how to handle unreliable delivery.

By the end of this article you'll understand the internal mechanics of both protocols, read and run working Java socket examples that show the difference in behaviour, and be able to confidently answer 'which protocol would you use and why?' for any use-case thrown at you — in an interview room or a design document.

How TCP Actually Guarantees Delivery — The Three-Way Handshake and Beyond

TCP's reliability isn't magic — it's engineering. Before a single byte of your data travels anywhere, TCP performs a three-way handshake: the client sends SYN, the server replies SYN-ACK, and the client confirms with ACK. Only then does data flow. This ceremony establishes sequence numbers on both sides, which is how TCP tracks every segment and detects anything that goes missing.

Once connected, every segment the sender transmits must be acknowledged by the receiver. If an ACK doesn't arrive within a timeout window, the segment is retransmitted automatically — your application never sees this retry logic because the OS handles it inside the kernel. TCP also uses flow control (the receiver advertises how much buffer space it has) and congestion control (the sender backs off when the network is overwhelmed). Together, these mechanisms make TCP self-healing but inherently slower.

The cost is latency. Each round trip for a handshake takes time. Each lost packet stalls the entire stream because TCP enforces in-order delivery — a phenomenon called Head-of-Line Blocking. For loading a bank statement or downloading a ZIP file, that's a perfectly acceptable trade-off. For a live video call, it's catastrophic.

Understanding this helps you make smarter architecture decisions. HTTPS runs on TCP because an incomplete HTML response is useless. DNS often uses UDP because a single question-answer fits in one packet and a retry is trivial if it fails.

TcpEchoServer.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.io.*;
import java.net.*;

/**
 * A minimal TCP echo server that demonstrates reliable, ordered delivery.
 * Run TcpEchoServer first, then run TcpEchoClient in a separate terminal.
 */
public class TcpEchoServer {

    private static final int PORT = 9090;

    public static void main(String[] args) throws IOException {

        // ServerSocket binds to a port and listens for incoming TCP connections.
        // The OS completes the three-way handshake before accept() returns —
        // by the time we get a Socket object, the connection is already established.
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {

            System.out.println("[Server] Listening on port " + PORT + " (TCP)");

            // accept() blocks until a client connects. Each call returns one
            // dedicated Socket for that client — full duplex, stream-oriented.
            try (Socket clientSocket = serverSocket.accept()) {

                System.out.println("[Server] Client connected from: "
                        + clientSocket.getInetAddress());

                // Wrap the raw byte stream in readers/writers for convenience.
                // The underlying stream guarantees every byte arrives in order.
                BufferedReader inFromClient = new BufferedReader(
                        new InputStreamReader(clientSocket.getInputStream()));
                PrintWriter outToClient = new PrintWriter(
                        clientSocket.getOutputStream(), true); // autoFlush = true

                String receivedMessage;
                // Read lines until the client closes the connection (readLine returns null).
                while ((receivedMessage = inFromClient.readLine()) != null) {
                    System.out.println("[Server] Received: " + receivedMessage);

                    // Echo the message back in uppercase so we can visibly confirm
                    // it made the round trip intact.
                    String response = "ECHO: " + receivedMessage.toUpperCase();
                    outToClient.println(response);
                    System.out.println("[Server] Sent back: " + response);
                }

                System.out.println("[Server] Client disconnected.");
            }
        }
    }
}
Output
[Server] Listening on port 9090 (TCP)
[Server] Client connected from: /127.0.0.1
[Server] Received: hello from tcp client
[Server] Sent back: ECHO: HELLO FROM TCP CLIENT
[Server] Client disconnected.
Why Head-of-Line Blocking Matters:
If packet #4 of a TCP stream is lost, packets #5, #6, and #7 sit in the receiver's buffer waiting — even though they arrived fine. HTTP/3 replaced TCP with QUIC specifically to eliminate this problem for web traffic. Knowing this nuance will impress any interviewer asking about modern protocol design.
Production Insight
In production, TCP retransmission storms can cascade: one dropped packet triggers fast retransmit, which triggers more ACKs, which consumes CPU. The OS handles retransmission, but your app still pays the latency penalty.
The fix isn't always a faster network — sometimes it's switching from TCP to QUIC or UDP for real-time streams.
Rule: If you see 'retransmit' in logs, check for packet loss upstream; don't blame the app.
Key Takeaway
TCP trades latency for reliability — every byte arrives eventually, but the stream stalls on any loss.
Handshake overhead is a one-time cost per connection, but connection state memory scales with concurrent users.
For high-frequency updates, head-of-line blocking kills interactive experience — choose UDP or QUIC instead.
When to Use TCP
IfData must arrive completely and in order
UseUse TCP — file transfers, database transactions, HTTP/HTTPS
IfSmall, frequent, latency-sensitive messages
UseConsider UDP or QUIC — TCP's head-of-line blocking hurts
IfNeed flow control and congestion control but can't implement yourself
UseUse TCP — kernel does it automatically

How UDP Works — Fast, Lightweight, and Deliberately Unreliable

UDP's design philosophy is the opposite of TCP's: get the data out as fast as possible and let the application decide what to do if something goes wrong. There is no handshake, no acknowledgement, no retransmission, and no ordering guarantee. You send a datagram (a self-contained packet) and it either arrives or it doesn't.

This sounds reckless, but it's actually brilliant for certain workloads. Consider a live video stream. A video frame from two seconds ago is worse than useless — it actively hurts the viewer experience. UDP lets the application discard late or missing frames and move on. The same logic applies to DNS lookups (tiny single-packet exchanges), online gaming (position updates become stale in milliseconds), and VoIP calls.

The UDP header is only 8 bytes (source port, destination port, length, checksum) compared to TCP's minimum 20 bytes. Combined with no connection setup overhead, UDP achieves dramatically lower latency. This is why modern protocols like QUIC (used by HTTP/3), DTLS (secure datagrams), and WebRTC are all built on UDP and implement their own selective reliability on top.

Here's the key mental model: UDP is a raw postcard with no tracking. If you need tracking, you add it yourself at the application layer — and only where you need it. That selective reliability is far more efficient than TCP's blanket guarantees.

UdpEchoServer.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import java.net.*;

/**
 * A UDP echo server. Notice there is NO accept(), NO handshake, NO connection.
 * The server just sits and waits for datagrams to land in its socket buffer.
 * Run UdpEchoServer first, then run UdpEchoClient in a separate terminal.
 */
public class UdpEchoServer {

    private static final int PORT = 9091;
    // Max UDP payload that safely avoids IP fragmentation on most networks.
    private static final int MAX_PACKET_SIZE = 1024;

    public static void main(String[] args) throws Exception {

        // DatagramSocket is connectionless — no client needs to connect first.
        // It simply listens on a port for any datagram that arrives.
        try (DatagramSocket serverSocket = new DatagramSocket(PORT)) {

            System.out.println("[Server] Listening on port " + PORT + " (UDP)");

            byte[] receiveBuffer = new byte[MAX_PACKET_SIZE];

            // UDP servers typically loop forever, processing one datagram at a time.
            while (true) {

                // DatagramPacket is both the envelope and the letter —
                // it carries the data AND the sender's address/port.
                DatagramPacket incomingPacket =
                        new DatagramPacket(receiveBuffer, receiveBuffer.length);

                // receive() blocks until a datagram arrives. Unlike TCP's readLine(),
                // each call here processes exactly one independent packet.
                serverSocket.receive(incomingPacket);

                String receivedMessage = new String(
                        incomingPacket.getData(),
                        0,
                        incomingPacket.getLength() // IMPORTANT: use actual length, not buffer length
                );

                System.out.println("[Server] Received datagram from "
                        + incomingPacket.getAddress() + ":" + incomingPacket.getPort()
                        + " — Message: " + receivedMessage);

                // To reply, we need the sender's address and port from the incoming packet.
                // There is no persistent connection object — we manually address each reply.
                String responseMessage = "ECHO: " + receivedMessage.toUpperCase();
                byte[] responseBytes = responseMessage.getBytes();

                DatagramPacket replyPacket = new DatagramPacket(
                        responseBytes,
                        responseBytes.length,
                        incomingPacket.getAddress(), // send back to whoever sent to us
                        incomingPacket.getPort()
                );

                serverSocket.send(replyPacket);
                System.out.println("[Server] Replied: " + responseMessage);
            }
        }
    }
}
Output
[Server] Listening on port 9091 (UDP)
[Server] Received datagram from /127.0.0.1:52341 — Message: hello from udp client
[Server] Replied: ECHO: HELLO FROM UDP CLIENT
Watch Out: The Buffer Length Trap
Always use incomingPacket.getLength() when converting the byte array to a String — NOT receiveBuffer.length. The buffer is pre-allocated at 1024 bytes, but the actual datagram might be 12 bytes. If you use the buffer length, you'll get 1012 null characters appended to every message, causing silent data corruption that's surprisingly hard to debug.
Production Insight
UDP packet loss is silent — send() succeeds even if the destination doesn't exist. In production, this means retry logic is your responsibility.
A common failure: a monitoring agent uses UDP and misses critical alerts because no one implemented ACKs.
Rule: Always implement application-level ACKs with timeouts when using UDP for essential data.
Key Takeaway
UDP is not 'unreliable' — it's 'no guarantees by default'. You add reliability where needed.
The 8-byte header and connectionless design make UDP the fastest transport, but every lost packet is invisible to the sender.
Production rule: if data is important, assign sequence numbers and implement selective retransmission on top of UDP.
When to Use UDP
IfReal-time or near-real-time updates
UseUse UDP — video streaming, online gaming, VoIP, live sports
IfSmall request-response fits in one packet
UseUse UDP — DNS, DHCP, NTP, SNMP
IfNeed to handle thousands of clients without per-connection state
UseUse UDP — one socket serves all, no fd limit per client

Choosing TCP or UDP in the Real World — A Decision Framework

The choice between TCP and UDP isn't about which is 'better' — it's about which constraints match your problem. Run through this mental checklist every time you're designing a networked feature.

Ask: 'What happens if a packet is lost?' If the answer is 'the entire operation is corrupt or meaningless' (file download, database query, user login), use TCP. If the answer is 'the app can recover or the data is stale anyway' (live video frame, game position update, telemetry ping), UDP is worth considering.

Ask: 'How many messages per second?' UDP's stateless nature means a single server socket can handle thousands of different senders without maintaining connection state for each. A gaming server receiving 60 position updates per second from 1,000 players would be crushed by the per-connection overhead of TCP.

Ask: 'Do I need ordered delivery or just fast delivery?' UDP datagrams can arrive out of order. If your application can sequence them itself (most game engines do this with a simple timestamp comparison), you get the speed of UDP with the ordering your game logic needs.

Finally: 'Are you reinventing a solved problem?' If you catch yourself implementing retransmission, acknowledgements, and flow control on top of UDP, you've essentially built a worse version of TCP. Use TCP instead, or use a battle-tested library like KCP or QUIC that already solves this correctly.

UdpEchoClient.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import java.net.*;

/**
 * Companion client for UdpEchoServer.
 * Demonstrates fire-and-forget sending and how to read a reply.
 * Also shows that if the server is down, send() returns immediately —
 * there is NO error thrown. That's the fundamental nature of UDP.
 */
public class UdpEchoClient {

    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 9091;
    private static final int TIMEOUT_MS = 3000; // how long to wait for a reply
    private static final int MAX_PACKET_SIZE = 1024;

    public static void main(String[] args) throws Exception {

        // A client DatagramSocket with no arguments lets the OS pick a free port.
        // Unlike TCP, this does NOT send anything to the server — no handshake.
        try (DatagramSocket clientSocket = new DatagramSocket()) {

            // Set a timeout on receive() so we don't block forever if the reply is lost.
            // This is manual reliability — something TCP does for you automatically.
            clientSocket.setSoTimeout(TIMEOUT_MS);

            InetAddress serverAddress = InetAddress.getByName(SERVER_HOST);
            String messageToSend = "hello from udp client";
            byte[] sendBuffer = messageToSend.getBytes();

            // Construct the datagram with the data AND the destination baked in.
            DatagramPacket sendPacket = new DatagramPacket(
                    sendBuffer,
                    sendBuffer.length,
                    serverAddress,
                    SERVER_PORT
            );

            // send() dispatches the packet and returns immediately.
            // If the server isn't running, this line still succeeds with no exception —
            // the packet just disappears into the network. This is UDP's core behaviour.
            clientSocket.send(sendPacket);
            System.out.println("[Client] Sent: " + messageToSend);

            // Prepare a buffer to receive the server's echo reply.
            byte[] receiveBuffer = new byte[MAX_PACKET_SIZE];
            DatagramPacket replyPacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);

            try {
                // receive() will block until a datagram arrives OR the timeout fires.
                clientSocket.receive(replyPacket);
                String reply = new String(replyPacket.getData(), 0, replyPacket.getLength());
                System.out.println("[Client] Received reply: " + reply);

            } catch (SocketTimeoutException timeoutEx) {
                // This is how UDP 'reliability' works at the app layer —
                // you catch the timeout and decide: retry, fail, or move on.
                System.out.println("[Client] No reply within " + TIMEOUT_MS + "ms — packet may be lost.");
            }
        }
    }
}
Output
[Client] Sent: hello from udp client
[Client] Received reply: ECHO: HELLO FROM UDP CLIENT
Pro Tip: When to Layer Reliability on UDP
If you need low latency AND some reliability (e.g., game events like 'player died' that must arrive but can't tolerate TCP's head-of-line blocking), look at Reliable UDP libraries like ENet or QUIC. They give you per-stream reliability without blocking the whole connection on a single lost packet — the exact problem that motivated HTTP/3's switch from TCP to QUIC.
Production Insight
Companies often default to TCP because it 'feels safer'. That choice can kill real-time features.
A telematics company lost 40% of vehicle updates because TCP retransmissions queued behind lost packets — stale GPS data was worse than missing data.
Rule: Benchmark both protocols under realistic packet loss (use netem to simulate) before deciding.
Key Takeaway
The decision is not TCP vs UDP — it's reliability vs latency vs state overhead.
Default to TCP for critical data, UDP for real-time streams, and layer selective reliability on UDP when needed.
Never build a custom reliable protocol on top of UDP unless you have a team that maintains it — use QUIC or KCP instead.
Decision Tree: TCP or UDP?
IfData must arrive intact?
UseYes → TCP (unless you can implement application ACKs). No → Consider UDP.
IfHigh message frequency (e.g., 60 updates/sec)?
UseUDP avoids connection state overhead. TCP may struggle with per-connection memory.
IfNeed ordered delivery but can reorder at app layer?
UseUse UDP + timestamp/sequence numbers. If you need in-order as received, TCP wins.

TCP Flow Control and Congestion Control — How the Kernel Keeps Things in Check

Beyond delivery guarantees, TCP has two critical mechanisms that UDP lacks entirely: flow control and congestion control. Flow control prevents the sender from overwhelming the receiver's buffer. The receiver advertises a window size (how many bytes it can accept) in every ACK. The sender respects that window — if the window drops to zero, the sender stops until further notice.

Congestion control prevents the sender from overwhelming the network. TCP uses algorithms like Reno, Cubic, or BBR to detect congestion through packet loss or RTT increase. When congestion is detected, TCP cuts its sending rate in half (multiplicative decrease) and slowly ramps back up (additive increase). This AIMD behavior is why TCP traffic is 'polite' — it yields to other flows when the network is busy.

UDP has none of this. A misbehaving UDP application can blast packets at line rate, causing bufferbloat at routers and starving other traffic on the same link. In production, this is why many enterprises rate-limit UDP traffic or enforce QoS policies.

Understanding these mechanisms helps you diagnose production network issues. If your TCP throughput is inconsistent, the culprit is often congestion control kicking in. Look at your TCP retransmission rate and flight size. For UDP, implement application-level pacing and backpressure to avoid flooding the network.

TcpClient.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.io.*;
import java.net.*;

/**
 * A TCP client that sends a message and reads the echoed response.
 * Demonstrates the stream-based, reliable nature of TCP.
 */
public class TcpClient {

    private static final String SERVER_HOST = "localhost";
    private static final int SERVER_PORT = 9090;

    public static void main(String[] args) throws IOException {

        // Socket constructor does the three-way handshake internally.
        // If the server is unreachable, this constructor throws ConnectException.
        try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT)) {

            // Get output stream to send data to the server.
            PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
            // Get input stream to read server responses.
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            String message = "hello from tcp client";
            out.println(message);
            System.out.println("[Client] Sent: " + message);

            String response = in.readLine(); // Blocks until server sends a line
            System.out.println("[Client] Received: " + response);
        }
    }
}
Output
[Client] Sent: hello from tcp client
[Client] Received: ECHO: HELLO FROM TCP CLIENT
TCP Window as a Faucet
  • Flow control = bucket size. Receiver says, 'I can hold 64KB right now.' Sender fills accordingly.
  • Congestion control = network pipe diameter. Sender probes the pipe size and reduces flow when it detects backpressure.
  • UDP = no bucket, no pipe — you just throw water and hope some gets to the other side.
  • When bucket overflows (receiver buffer full), TCP pauses. When UDP overflows, packets drop silently.
Production Insight
A common production trap: TCP throughput drops to near zero because the receiver's socket buffer is too small (default 64KB on some systems).
Increase SO_RCVBUF to tens of megabytes for high-throughput streams like video uploads.
Rule: Monitor TCP window scale and socket buffer drops via netstat -s or /proc/net/tcp before blaming the network.
Key Takeaway
Flow control prevents receiver overwhelm; congestion control prevents network collapse.
TCP dynamically adapts to network conditions; UDP requires you to implement both at the application layer.
The kernel's congestion algorithms (CUBIC, BBR) are production-tested — don't reimplement unless you must.

Real-World Protocol Selection — Case Studies and Trade-offs

Let's look at concrete examples where the TCP/UDP choice made or broke a system:

Case Study 1: Video Conferencing (Zoom, WebRTC) — Uses UDP almost exclusively. A lost video frame is replaced by the previous frame — invisible to the user. TCP would cause stuttering because every retransmitted frame arrives too late. Audio is even more sensitive: any delay >150ms is noticeable. WebRTC adds FEC (Forward Error Correction) to recover lost packets without retransmission.

Case Study 2: Online Banking — All TCP/HTTPS. Every request must be atomic and complete. A missing byte in a transfer instruction would be catastrophic. The overhead of connection setup is negligible compared to the cost of inconsistency.

Case Study 3: IoT Sensor Telemetry — Most sensors send small readings (temperature, pressure) every few seconds. UDP works well because losing an occasional reading is acceptable. But critical alarms (fire detection) need reliable delivery — those should be sent over TCP or with application-level ACKs.

Case Study 4: DNS — Classic UDP success story. A single query/response fits in 512-4096 bytes. UDP eliminates handshake overhead. If a response is lost, the resolver retries after a timeout — functionally equivalent to TCP's retransmission but without connection state. Large responses (DNSSEC) fall back to TCP.

The pattern: Protocols evolve from TCP to UDP when they hit head-of-line blocking or per-connection scaling limits. HTTP/3's move to QUIC (UDP) is the most visible example.

The QUIC Revolution
QUIC (Quick UDP Internet Connections) is the modern transport that sits on UDP but provides TCP-like reliability, flow control, and encryption — without head-of-line blocking. It's the backbone of HTTP/3. If you're building a new network protocol today, start with QUIC rather than TCP.
Production Insight
A common mistake: assuming 'UDP is unreliable' means you can't use it for important data.
The truth: you can layer selective reliability on UDP for the subset of messages that matter.
Rule: Identify which messages are critical vs. real-time — send critical ones with ACK and timeout, real-time ones fire-and-forget.
Key Takeaway
Protocol choice evolves with application needs — QUIC demonstrates that UDP + selective reliability often wins over pure TCP.
The cost of implementing custom reliability on UDP is justified when you need sub-millisecond latency for the majority of packets.
Performance rule: For loss-tolerant data, UDP reduces tail latency by 40-80% compared to TCP under real-world packet loss conditions.
● Production incidentPOST-MORTEMseverity: high

The 3 AM Blizzard: TCP Connection Exhaustion Behind a Gaming Server

Symptom
Server CPU at 40%, but new players get 'Connection Refused' errors. Active players experience intermittent lag spikes followed by disconnects.
Assumption
The bottleneck must be CPU or network bandwidth — we need bigger instances.
Root cause
TCP maintains a socket struct per connection (~3KB on Linux). With 5000 concurrent players and each sending 20 updates/sec, the server hit the file descriptor limit (ulimit -n 1024 default) and also exhausted kernel memory for connection tracking. The OS refused new connections silently.
Fix
Increased file descriptor limits (ulimit -n 65536), enabled TCP quickack, and most importantly switched the position update stream to UDP. Only critical events (achievements, purchases) stayed on TCP. Connection count dropped from 5000 to ~500.
Key lesson
  • TCP's per-connection state scales linearly with concurrent connections — not with throughput. For high-frequency, stateless updates, UDP eliminates this scaling tax.
  • Always benchmark connection overhead under realistic load before going to production.
  • Use kernel tuning (net.core.somaxconn, net.ipv4.tcp_tw_reuse) alongside protocol choice.
Production debug guideDiagnose and resolve network protocol issues in real systems4 entries
Symptom · 01
TCP connection timeouts / slow handshake completion
Fix
Check netstat -s for 'SYN to SYN+ACK retransmits'. High count indicates congestion or firewall dropping SYNs. Use tcpdump to inspect handshake packets. Lower the initial RTO via net.ipv4.tcp_syn_retries.
Symptom · 02
UDP packets arriving but application sees no data
Fix
Verify socket buffer size with net.core.rmem_default and /proc/sys/net/ipv4/udp_mem. Dropped packets show as 'RcvbufErrors' in netstat -s. Increase buffer with setsockopt SO_RCVBUF.
Symptom · 03
Application logs show TCP retransmissions (duplicate ACKs)
Fix
Enable TCP trace: tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0'. Look for DUP ACKs and fast retransmit patterns. Common cause: asymmetrical routing or network interface saturation.
Symptom · 04
UDP send() returns success but receiver never gets the packet
Fix
UDP is connectionless — send() only places packet in transmit queue. Use tcpdump on both sides to confirm packet leaves sender and arrives at receiver. If only sender shows it, check firewall/NAT. If receiver shows it but app doesn't see it, check socket buffer overflow with netstat -su.
★ Quick TCP/UDP Debugging Cheat SheetCommands and checks for common network protocol issues in production
TCP connection refused
Immediate action
Check if port is listening: ss -tlnp | grep <port>
Commands
ss -tlnp | grep 8080
curl -v telnet://localhost:8080
Fix now
Ensure service is running and not binding to 127.0.0.1 only.
TCP handshake completes but data never arrives+
Immediate action
Capture packets on both sides: tcpdump -i any port 8080 -w capture.pcap
Commands
tcpdump -i any port 8080 -nn -X
wireshark capture.pcap (filter tcp.analysis.lost_segment)
Fix now
Check firewall rules, MTU mismatch, or asymmetric routing.
UDP packet loss between services+
Immediate action
Check receiver socket buffer drops: netstat -s | grep udp
Commands
netstat -su | grep -E 'packet receive errors|RcvbufErrors'
ss -unap | grep <port>
Fix now
Increase socket buffer: sysctl -w net.core.rmem_max=26214400; sysctl -w net.core.rmem_default=26214400
High latency on TCP streams (no packet loss)+
Immediate action
Check tcp_info from /proc: cat /proc/net/tcp
Commands
ss -ti 'sport = :8080'
iptraf-ng or nethogs
Fix now
Enable TCP_NODELAY (Nagle's algorithm off) on the socket.
TCP vs UDP — Side-by-Side Comparison
Feature / AspectTCPUDP
Connection modelConnection-oriented (three-way handshake required)Connectionless (no setup, fire and forget)
Delivery guaranteeGuaranteed — lost segments are automatically retransmittedNo guarantee — packets may be lost silently
Ordering guaranteeYes — bytes always arrive in send orderNo — datagrams may arrive out of order
Error checkingChecksum + acknowledgement + retransmitChecksum only (no recovery on failure)
Speed / LatencyHigher latency due to handshake and ACK overheadLower latency — minimal overhead per packet
Header size20–60 bytes (minimum 20)8 bytes fixed
Flow controlYes (sliding window, receiver advertises buffer size)No — sender can overwhelm the receiver
Congestion controlYes (slow start, AIMD algorithms built into kernel)No — application must implement if needed
State on serverPer-connection state maintained by OSStateless — one socket handles all senders
Typical use casesHTTP/HTTPS, email (SMTP), file transfer (FTP/SFTP), SSHDNS, live video/audio streaming, online gaming, VoIP, IoT telemetry
Modern protocol built on itHTTP/1.1, HTTP/2, TLS (over TCP)QUIC (HTTP/3), DTLS, WebRTC, DNS-over-UDP

Key takeaways

1
TCP's reliability comes from sequence numbers + acknowledgements + retransmission, all handled by the OS kernel
your application code never sees the retry logic, but you always pay the latency cost for it.
2
UDP's superpower is statelessness
one server socket handles unlimited senders with zero per-connection overhead, which is why DNS, game servers, and streaming services choose it despite the lack of guarantees.
3
The real question isn't 'which is better?'
it's 'what happens in my application if a packet is lost?' If the answer is 'catastrophic', use TCP. If the answer is 'move on', use UDP.
4
HTTP/3 runs on QUIC which runs on UDP
meaning the future of the web runs on UDP, not TCP. Modern protocol design layers selective reliability on UDP rather than accepting TCP's head-of-line blocking.
5
Hybrid architectures are common
use UDP for real-time data streams and TCP for control messages or critical events. Don't feel forced to pick one protocol for the entire system.
6
Always test your protocol choice under realistic packet loss conditions using tools like Linux netem before production. A protocol that works in the lab may fail under 1% packet loss.

Common mistakes to avoid

5 patterns
×

Defaulting to TCP for everything because it 'feels safer'

Symptom
A real-time game or video stream has noticeable lag spikes because stale retransmitted packets delay the delivery of newer, more relevant data.
Fix
Identify whether stale data is worse than no data. If a 200ms-old position update is useless, use UDP and let the application discard outdated packets by comparing timestamps.
×

Using the receive buffer length instead of the datagram length when reading UDP data

Symptom
Every string parsed from a UDP packet ends with hundreds of null characters ('\u0000'), causing string comparison failures and garbled logs.
Fix
Always construct your String with new String(packet.getData(), 0, packet.getLength()) — the third argument caps the read at the actual received bytes, not the pre-allocated buffer size.
×

Assuming send() on a UDP socket throws an exception if the server is unreachable

Symptom
UDP client appears to work fine in testing but silently drops all messages in production when the server is down, with no log output.
Fix
Always implement an application-level acknowledgement with a timeout (setSoTimeout) and a retry counter. UDP's send() returns successfully even when the destination doesn't exist — reliability is your responsibility, not the protocol's.
×

Neglecting socket buffer sizes for high-throughput UDP pipelines

Symptom
UDP packet loss under production load even though the network is fine — packets are dropped at the receiver because the socket buffer overflowed.
Fix
Increase socket receive buffer (SO_RCVBUF) to at least 16MB for high-frequency UDP streams. Monitor netstat -su for 'RcvbufErrors'.
×

Using TCP_NODELAY without understanding Nagle's algorithm interaction with delayed ACKs

Symptom
TCP throughput dropped by 90% in some cases due to Nagle waiting for full segments and Delayed ACK waiting for data to ACK — both waiting for each other.
Fix
Enable TCP_NODELAY on the sender AND TCP_QUICKACK on the receiver for latency-sensitive workloads. Or use TCP_CORK for bulk sends.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
You're building a multiplayer racing game. Players send their car's posi...
Q02SENIOR
Explain what Head-of-Line Blocking is in TCP and describe a real-world p...
Q03SENIOR
A candidate says 'UDP is faster than TCP.' Is that always true? Describe...
Q04SENIOR
How does TCP flow control differ from congestion control? Give an exampl...
Q05SENIOR
Explain the three-way handshake and describe a scenario where a SYN floo...
Q01 of 05SENIOR

You're building a multiplayer racing game. Players send their car's position 60 times per second. Would you use TCP or UDP, and why? What would you do about important game events like 'player finished the race'?

ANSWER
Use UDP for position updates. Each update is tiny (a few bytes), and a lost update is immediately superseded by the next one. TCP would cause head-of-line blocking — a lost position packet delays all subsequent updates. For critical events like finish line crossings, use a separate TCP connection or add application-level ACKs with retry on top of UDP. This hybrid approach gives you low latency for real-time data and reliability for events that must arrive.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Is UDP faster than TCP?
02
Can UDP lose data? What happens when it does?
03
Why does DNS use UDP instead of TCP if reliability matters?
04
What is QUIC and why is it built on UDP instead of TCP?
05
Can I use both TCP and UDP in the same application?
🔥

That's Computer Networks. Mark it forged?

6 min read · try the examples if you haven't

Previous
TCP/IP Model
4 / 22 · Computer Networks
Next
HTTP and HTTPS Explained