← System Design Simulator

TCP vs UDP

By Rahul Kumar · Senior Software Engineer · Updated · Category: Networking + Web Security

3-way handshake, ordered delivery + retransmit, vs fire-and-forget.

This interactive explanation is built for system design interview prep: step through TCP vs UDP, watch the internal state change, and connect the concept to real distributed-system trade-offs.

Overview

TCP and UDP are the two transport protocols that carry almost every packet on the internet, and they make opposite trade-offs. TCP is a reliable, ordered, connection-oriented stream: every byte you write is guaranteed to arrive, in order, or you get an error. UDP is a best-effort datagram: you send a packet and hope it arrives. TCP gives you retransmission, flow control, congestion control, and connection state, which cost a three-way handshake (1 RTT before any data) and a per-connection buffer on both sides. UDP gives you none of that, which saves the handshake entirely — you send data with the first packet — and makes UDP the right choice when latency matters more than reliability (DNS, VoIP, video, game state) or when you want to build your own reliability on top (QUIC, custom protocols). Knowing which one to use and how each behaves under loss is fundamental to designing any networked system.

TCP vs UDP — Interactive Simulator

Runs fully client-side in your browser; no sign-up. Or open full screen →

Launch the interactive TCP vs UDP widget — step through the algorithm or protocol and observe the internal state updating in real time.

How it works

A TCP connection opens with a three-way handshake. The client sends SYN with its initial sequence number x; the server responds SYN-ACK with its own y and ack x+1; the client sends ACK y+1. At this point both sides have agreed on starting sequence numbers and window sizes. Data transfer uses sequence and acknowledgment numbers to detect loss; every segment is acked, and unacked segments are retransmitted after a timeout (RTO) or after three duplicate acks (fast retransmit). Flow control uses a receive window advertised by the receiver; congestion control (CUBIC, BBR) uses a congestion window maintained by the sender that grows on success and shrinks on loss. Connection close is a four-way handshake (FIN, ACK, FIN, ACK) or a one-shot RST. TCP buffers data on both sides: a slow reader stalls the sender because the receive window closes. UDP has no handshake, no sequence numbers, no acks, no buffering. A UDP send becomes a single IP packet (or fragmented if larger than MTU) and is handed to the network immediately. The sender does not know if it arrived. The receiver gets a datagram or nothing. Reorderings, duplicates, and losses are all silent. Applications that use UDP either do not care (DNS retries on timeout; VoIP tolerates lost samples) or implement their own reliability (QUIC has TCP-equivalent reliability on top of UDP, plus 0-RTT resumption). NIO in Java exposes both via SocketChannel (TCP) and DatagramChannel (UDP).

Implementation

TCP SocketChannel: connect, send, read
public class TcpClient {
    public static String fetch(String host, int port, String req) throws IOException {
        try (SocketChannel ch = SocketChannel.open()) {
            ch.configureBlocking(true);
            ch.connect(new InetSocketAddress(host, port)); // 3-way handshake here
            ch.write(ByteBuffer.wrap(req.getBytes(StandardCharsets.UTF_8)));

            ByteBuffer buf = ByteBuffer.allocate(4096);
            StringBuilder out = new StringBuilder();
            while (ch.read(buf) > 0) {
                buf.flip();
                out.append(StandardCharsets.UTF_8.decode(buf));
                buf.clear();
            }
            return out.toString();
        }
    }

    public static void main(String[] args) throws IOException {
        String resp = fetch("example.com", 80,
            "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");
        System.out.println(resp.substring(0, Math.min(200, resp.length())));
    }
}
UDP DatagramChannel: send-and-forget
public class UdpClient {
    public static void sendAndForget(String host, int port, byte[] payload) throws IOException {
        try (DatagramChannel ch = DatagramChannel.open()) {
            // No connect/handshake required for UDP.
            ch.send(ByteBuffer.wrap(payload), new InetSocketAddress(host, port));
            // Receiver may or may not have gotten it; we don't know.
        }
    }

    /** Optional: 'connect' a datagram channel to set a default peer; still no handshake. */
    public static ByteBuffer requestResponse(String host, int port, byte[] req, int timeoutMs)
            throws IOException {
        try (DatagramChannel ch = DatagramChannel.open()) {
            ch.connect(new InetSocketAddress(host, port));
            ch.write(ByteBuffer.wrap(req));

            ByteBuffer resp = ByteBuffer.allocate(1500); // MTU-sized
            ch.socket().setSoTimeout(timeoutMs);
            ch.read(resp);
            resp.flip();
            return resp;
        }
    }
}

Complexity

Key design decisions & trade-offs

Common pitfalls

Interview follow-ups

Recommended reading

Related