📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is the difference between TCP and UDP? When would you choose each, and why does HTTP/3 run over UDP?

TCP and UDP compared: connections, reliability, ordering, flow and congestion control, head-of-line blocking, typical uses, and QUIC/HTTP/3 in .NET.

Mid-Level Theory Intermediate http3 networking quic sockets tcp udp

🎯 Key points

  • TCP is connection-oriented: a three-way handshake, then a reliable, ordered byte stream with retransmission, flow control (receive window) and congestion control (CUBIC, BBR).
  • UDP is connectionless: independent datagrams with an 8-byte header, message boundaries kept, but no reliability, ordering, duplicate removal or congestion control.
  • TCP has no message boundaries, so protocols must frame messages (length prefix or delimiter); HTTP, gRPC and WebSockets do this for you.
  • TCP head-of-line blocking means one lost packet stalls everything behind it, which hurts real-time traffic and stalls every HTTP/2 stream on the connection.
  • QUIC runs on UDP and integrates TLS 1.3 into its own handshake (1-RTT, 0-RTT on resumption), orders each stream independently, and supports connection migration when a client's IP changes.
  • QUIC uses UDP because TCP lives in OS kernels and middleboxes and is nearly impossible to change, while UDP passes through and lets QUIC evolve in user space.
  • Keep UDP datagrams under the path MTU (about 1,200 to 1,400 bytes of payload); larger ones fragment and one lost fragment loses the whole datagram.
  • Choose TCP (or HTTP/2) for APIs, databases and data transfer, UDP for DNS, real-time media, games and loss-tolerant metrics; in .NET, HTTP/3 uses msquic, needs UDP 443 open and should fall back to HTTP/2.

⏱ 30-second answer

  • TCP is connection-oriented and gives a reliable, ordered byte stream: handshake, acknowledgements, retransmission, flow control and congestion control. Cost: a setup round trip and head-of-line blocking (one lost packet stalls everything behind it).
  • UDP sends independent datagrams with no connection, no ordering and no retransmission. It is lightweight and low-latency, and the application decides what to do about loss.

Use TCP for most APIs, databases and file transfer. Use UDP for DNS, real-time media, games, metrics (StatsD) and as the base for QUIC. HTTP/3 runs on QUIC over UDP so it can combine TLS 1.3 with the transport handshake, avoid TCP's head-of-line blocking between streams, and survive network changes.

🗣 Full answer (senior)

Comparison

TCP UDP
Connection Three-way handshake None
Unit Byte stream (no message boundaries) Datagrams (boundaries kept)
Reliability Retransmits lost segments None; packets may be lost
Ordering Guaranteed Not guaranteed
Duplicates Removed Possible
Flow/congestion control Built in None (app must be polite)
Header 20–60 bytes 8 bytes
Typical uses HTTP/1.1 and HTTP/2, SQL, SMTP, SSH, gRPC DNS, VoIP, video, games, StatsD, QUIC/HTTP/3
.NET TcpClient, Socket (Stream), NetworkStream UdpClient, Socket (Dgram)

Head-of-line blocking

HTTP/2 over TCP: streams A, B, C share one TCP connection
packets:  A1 B1 C1 [A2 lost] B2 C2
TCP delivers in order, so B2 and C2 wait until A2 is retransmitted → all streams stall

HTTP/3 over QUIC (UDP): each stream ordered independently
A waits for A2; B2 and C2 are delivered immediately

Why QUIC uses UDP

Changing TCP itself is almost impossible because it lives in every OS kernel and middlebox. UDP passes through networks untouched, so QUIC implements reliability, congestion control and streams in user space on top of it, with TLS 1.3 built in. Benefits: 1-RTT setup (0-RTT on resumption), no cross-stream head-of-line blocking, and connection migration when a phone switches from Wi-Fi to mobile data.

Production example

A telemetry agent sent metrics to a collector over TCP, one connection per agent. During a collector slowdown, TCP back-pressure blocked the agent's send calls, and because the agent sent synchronously from request threads, the application slowed down too. We switched to UDP StatsD-style fire-and-forget for counters, accepting rare loss, and kept TCP (OTLP over gRPC with batching and a bounded queue) for traces. The app was no longer coupled to the collector's health.

🔍 What the interviewer is checking

Checks transport-layer fundamentals and the ability to choose protocols based on requirements.

Green flags

  • Explains reliability, ordering, flow and congestion control in TCP.
  • Knows UDP has message boundaries and no guarantees.
  • Explains head-of-line blocking and why QUIC uses UDP.
  • Mentions message framing over TCP.

Red flags

  • "UDP is just faster TCP."
  • Assumes TCP preserves message boundaries.

📚 Learn in depth

1. TCP connection lifecycle

Client                         Server
  │── SYN seq=x ───────────────────► │  LISTEN
  │◄── SYN-ACK seq=y ack=x+1 ────────│
  │── ACK ack=y+1 ──────────────────► │  ESTABLISHED
  │── data ─────────────────────────► │
  │◄── ACK ───────────────────────── │
  │── FIN ─────────────────────────► │  (graceful close, TIME_WAIT on the closer)

2. How TCP stays reliable and fair

  • Sequence numbers + ACKs: the sender retransmits unacknowledged data after a timeout or duplicate ACKs.
  • Flow control: the receiver advertises a window (how much it can buffer), so a fast sender cannot overwhelm a slow reader.
  • Congestion control (CUBIC, BBR): the sender starts slowly and backs off on loss, sharing the network fairly.
  • Nagle's algorithm combines small writes; it adds latency for chatty protocols. Socket.NoDelay = true disables it (Kestrel and HttpClient already do this).

3. TCP is a stream, not messages

sender: Send("HELLO") Send("WORLD")
receiver may get: "HELLOWOR" then "LD"   or   "HELLOWORLD"   or  "HEL" "LOWORLD"

Your protocol must frame messages (length prefix or delimiter). HTTP, gRPC and WebSockets do this for you.

4. UDP realities

  • Max datagram is 65,507 bytes, but anything over the path MTU (~1,472 bytes of payload on Ethernet) is fragmented, and losing one fragment loses the datagram. Keep datagrams small.
  • No congestion control: blasting UDP can hurt other traffic and get rate-limited.
  • Firewalls often block or time out UDP flows faster than TCP.
  • DNS uses UDP for small queries and falls back to TCP for large answers.

5. HTTP/3 in .NET

  • Kestrel and HttpClient support HTTP/3 via System.Net.Quic, which uses msquic (built into Windows 11 / Server 2022; on Linux install the libmsquic package).
  • Clients discover HTTP/3 through the Alt-Svc header, then try QUIC on UDP 443, falling back to HTTP/2 if UDP is blocked.
  • HttpClient needs request.Version = HttpVersion.Version30 and a version policy to try it.

6. Try it yourself

  1. Start a UdpClient listener and send 1,000 numbered datagrams from another app over Wi-Fi; count how many arrive and whether order changes.
  2. Do the same over TCP and note it always arrives complete and ordered.
  3. Capture both in Wireshark and compare handshakes and headers.
  4. curl --http3 -I https://cloudflare.com (curl built with HTTP/3) and inspect the alt-svc header with curl -I.

💻 Code

// UDP: fire-and-forget metric (StatsD-style). No connection, no delivery guarantee.
using var udp = new UdpClient();
udp.Connect("metrics.internal", 8125);          // sets default remote; no handshake
byte[] payload = Encoding.ASCII.GetBytes("orders.created:1|c");
await udp.SendAsync(payload);                   // returns immediately even if nobody is listening
// TCP: framed messages over a stream (length prefix), because TCP has no message boundaries
using var client = new TcpClient { NoDelay = true };
await client.ConnectAsync("ledger.internal", 9000, ct);
await using var stream = client.GetStream();

static async Task SendMessageAsync(Stream s, ReadOnlyMemory<byte> body, CancellationToken ct)
{
    var header = new byte[4];
    BinaryPrimitives.WriteInt32BigEndian(header, body.Length);
    await s.WriteAsync(header, ct);
    await s.WriteAsync(body, ct);
}

static async Task<byte[]> ReadMessageAsync(Stream s, CancellationToken ct)
{
    var header = new byte[4];
    await s.ReadExactlyAsync(header, ct);
    var body = new byte[BinaryPrimitives.ReadInt32BigEndian(header)];
    await s.ReadExactlyAsync(body, ct);
    return body;
}
// HttpClient trying HTTP/3 with fallback
using var http = new HttpClient
{
    DefaultRequestVersion = HttpVersion.Version30,
    DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower
};
using var response = await http.GetAsync("https://example.com/", ct);
Console.WriteLine(response.Version); // 3.0 if QUIC worked, else 2.0 or 1.1

⚠ Common mistakes

Mistake: Saying UDP is simply "faster". Why it is wrong: It has less overhead and no handshake, but the application then owns loss, ordering and pacing, and blasting UDP without congestion control hurts other traffic. Instead: Choose UDP when late data is useless or loss is acceptable, and TCP or QUIC when every byte matters.

Mistake: Using TCP for hard real-time data. Why it is wrong: A single lost packet blocks newer data until it is retransmitted, so voice, video or game state arrives late instead of being skipped. Instead: Use UDP-based protocols (RTP, WebRTC, QUIC datagrams) that can drop or replace stale data.

Mistake: Treating TCP reads as messages. Why it is wrong: One Send can arrive as several reads, or several sends as one read. Instead: Frame messages with a length prefix or delimiter and read with ReadExactlyAsync.

Mistake: Sending large UDP datagrams. Why it is wrong: Anything over the path MTU fragments at the IP layer, and losing any fragment loses the whole datagram; many firewalls also drop fragments. Instead: Keep payloads under about 1,200 to 1,400 bytes.

Mistake: Assuming HTTP/3 always works. Why it is wrong: Corporate firewalls and some load balancers block or rate-limit UDP 443, and Linux hosts need libmsquic installed. Instead: Use HttpVersionPolicy.RequestVersionOrLower so clients fall back to HTTP/2, and verify UDP 443 end to end.

🏛 Senior insight: trade-offs

The real trade-off is guarantees versus coupling and latency. TCP's guarantees come with back-pressure and head-of-line blocking, which couple sender to receiver and one stream to another.

Need Protocol Why
Internal APIs, gRPC HTTP/2 over TCP Mature, multiplexed, well supported by proxies
Mobile or lossy clients HTTP/3 over QUIC Per-stream recovery, faster setup, connection migration
Real-time media, games UDP (RTP, WebRTC) Late data is worthless; skip rather than wait
Loss-tolerant metrics UDP (StatsD) Never blocks the app when the collector is slow
Traces and logs TCP with a bounded background exporter Need completeness, but must not block requests

Architecturally, protocol choice shapes failure behaviour: TCP back-pressure can spread a slow consumer's problem into your request threads, while UDP pushes the decision about loss into your code. I keep telemetry off request paths, treat HTTP/3 as an optimisation with fallback, and measure before claiming benefits. Most internal service traffic is still best served by HTTP/2 over well-reused TCP connections.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions HTTP/2 or HTTP/3 Why: Checks HTTP versions and semantics
    How to answer

    HTTP is a request/response protocol: method + path + headers (+ body) in, status + headers (+ body) out.

    • Methods: GET/HEAD/OPTIONS are safe; GET, PUT, DELETE are idempotent (repeating has the same effect); POST and usually PATCH are not, so retries need an idempotency key.
    • Status codes: 2xx success, 3xx redirect/not modified, 4xx client error (400, 401 vs 403, 404, 409, 422, 429), 5xx server error (500, and 502/503/504 usually from proxies or gateways).
    • Headers: Content-Type, Accept, Authorization, Cache-Control, ETag/If-None-Match, Retry-After, traceparent.
    • Versions: HTTP/1.1 is text with one request at a time per connection; HTTP/2 is binary with multiplexed streams and header compression on one TCP connection; HTTP/3 does the same over QUIC/UDP, removing TCP head-of-line blocking.
  2. When: Candidate mentions TIME_WAIT or connection close Why: Probes socket exhaustion debugging
    How to answer

    Two different limits produce similar errors:

    1. Descriptor exhaustion (EMFILE, "Too many open files"): the process hit its ulimit -n. Something is leaking files or sockets.
    2. Ephemeral port exhaustion (EADDRNOTAVAIL, "Cannot assign requested address"): too many outbound connections to the same destination, usually closed sockets stuck in TIME_WAIT for 60 s.

    The classic .NET cause is new HttpClient() per request. Fix it with a shared HttpClient using SocketsHttpHandler { PooledConnectionLifetime = ... }, or IHttpClientFactory. Diagnose with ls /proc/<pid>/fd | wc -l, lsof -p, ss -s and ss -tan state time-wait.

  3. When: Candidate mentions ports or sockets Why: Checks socket fundamentals
    How to answer

    A port is a 16-bit number (0 to 65535) that lets one IP address host many services. A socket is the OS object for one endpoint: a listening socket bound to IP:port, and a connected socket for each accepted connection, identified by the 4-tuple (client IP, client port, server IP, server port).

    The Docker problem is almost always binding: the app listens on localhost (127.0.0.1) inside the container, which is unreachable from outside its network namespace, or it listens on a different port from the one you mapped. The official .NET 10 images set ASPNETCORE_HTTP_PORTS=8080, which binds all interfaces on 8080 (the default since .NET 8), so map it with -p 5000:8080 and do not override it with a localhost URL.

🛠 Real-world problems: how you handle them

Application slowed by synchronous TCP telemetry during a collector outage
Situation
[Checkout API] on .NET [10] sending custom metrics over a TCP connection to a [metrics collector] from request handlers.
Signals
When the collector slowed, checkout p99 rose from [80 ms] to [2 s]; thread pool queue length grew; no errors in the app's own code.
Investigation
Dumps showed many threads blocked in `NetworkStream.Write`. The collector's TCP receive window was full, so writes blocked through back-pressure.
Resolution
Moved counters to UDP fire-and-forget and traces to OpenTelemetry with a bounded background exporter that drops on overflow.
Prevention
Rule: telemetry must never block request paths; dashboards for exporter queue drops; chaos test stopping the collector.

How to tell it (STAR)

**S:** Our [checkout API] slowed down every time the [metrics collector] had problems. **T:** I needed to decouple the app from its telemetry. **A:** A dump showed request threads blocked writing metrics over TCP, because TCP back-pressure stalls the sender when the receiver is slow. I moved counters to UDP, which never blocks, and traces to a bounded background exporter. **R:** A later collector outage had [zero] effect on checkout latency, and we only lost a small number of metrics.

↩ Usually asked after

More in Networking Basics

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me