System Design
B1

Networking

A request costs round trips before it costs bandwidth. Count the round trips each protocol needs, then remove them: reuse connections, end TLS near the user, and send fewer bytes.

Not startedSaved in this browser only.
  1. 1Latency is round trips times the RTT. A new HTTPS connection costs 3 round trips with TLS 1.3, 4 with TLS 1.2.
  2. 2Reuse connections: a warm connection costs 1 round trip per request and far less CPU than a handshake.
  3. 3HTTP/2 removes HTTP head-of-line blocking; TCP loss still holds every stream. QUIC removes that too.
  4. 4The speed of light sets the floor: about 1 ms of RTT per 100 km of fiber.
B1
    A

    What one request costs

    measured at an 80 ms RTT
    ClientServerSYNSYN-ACKRTT 1TCP 81 msClientHello + key shareServerHello … FinishedRTT 2TLS 1.3 84 msFinished + GET /200 OKRTT 3request 82 msNew connection: 3 round trips, 248 msReused connection: GET and 200 OK only, 1 round trip, 81 ms
    • Round-trip time (RTT): a packet's trip to the server and back.
    • Each handshake waits for a reply, so it costs whole round trips.
    • Bandwidth does not shorten a round trip.

    A new HTTPS connection costs three round trips with TLS 1.3 before the response starts. A reused connection costs one.

    B

    Round trips before the response

    new connection against reused
    setupnewreusedstatus
    Plain HTTP over TCP21Not on the internet
    TCP + TLS 1.241Old clients only
    TCP + TLS 1.331Approved
    QUIC: HTTP/321Approved
    QUIC with 0-RTT data11Idempotent requests only
    TLS 1.3 with 0-RTT data21Idempotent requests only

    TLS 1.2, TLS 1.3 and HTTP/2: measured in the lab. QUIC and 0-RTT: derived from RFC 8446, RFC 9000 and RFC 9001. A DNS miss adds lookups before all of them.

    I count round trips: 4 for TLS 1.2, 3 for TLS 1.3, 2 for HTTP/3, and 1 on any warm connection.

    C

    A latency budget across an ocean

    derived: distance, then round trips
    Frankfurt → Virginia · RTT 66 ms (light in fiber) · server 50 msTLS 1.2, newdirect to VirginiaTCPTLS ×2GETserver314 msTLS 1.3, newdirect to VirginiaTCPTLSGETserver248 msHTTP/3, newQUIC, directQUIC+TLSGETserver182 msEdge, newedge 10 ms, warm linkGETserver146 msReusedwarm connectionGETserver116 msLower bounds: real fiber paths are longer. A DNS miss adds lookups before the TCP handshake.
    distance
    Frankfurt to northern Virginia: 6,550 km on the great circle.
    fiber
    Light in glass covers about 200 km per ms. RTT ≥ 2 × 6,550 / 200 ≈ 66 ms.
    assumed
    Edge 10 ms away; the server takes 50 ms. Both are example values.

    Frankfurt to Virginia is at least 66 ms of RTT. A cold TLS 1.3 request pays it three times, so I end TLS at a nearby edge and keep the long link warm.

    D

    Measured: TLS 1.2, TLS 1.3, reuse

    5 requests at 80 ms
    5 requeststotalconns
    TLS 1.2, new each1,637 ms5
    TLS 1.3, new each1,240 ms5
    TLS 1.3, reused582 ms1
    • Model: 20, 15 and 7 round trips. 7 × 80 ms = 560 ms.
    • At a 1 ms RTT, the TLS 1.2 handshake took 3.9 ms and TLS 1.3 3.2 ms: CPU, not distance.

    At an 80 ms RTT, five requests on one warm connection finished in less than half the time of five new TLS 1.3 connections.

    E

    The path of one request

    click a step; its path lights up
    Clientbrowser or appDNS resolvercaches by TTLLoad balancerends TLSServicekeep-alive poolPostgresconnection pool

    Step 1: DNS lookup

    • Ask the resolver for the address of the name.
    • A cached answer costs nothing on the wire. A miss walks the DNS tree.
    • The answer is kept for its TTL.

    If it fails

    A long TTL keeps clients on the old address after a failover, until the TTL runs out.

    DNS is cached, the two handshakes end at the load balancer, and everything behind the balancer runs on warm, pooled connections.

    F

    Try it: round trips on a timeline

    recorded from real servers through a delaying proxy
    protocol
    round-trip time
    connection

    HTTP/1.1 over TLS 1.3. The client sends its key share in the first hello, so the handshake takes 1 round trip.

    0250500750100012501500ms since the first request →req 13 RTTreq 23 RTTreq 33 RTTreq 43 RTTreq 53 RTT
    TCP handshake TLS handshake request and response
    request 1TCP handshakeTLS handshakerequest and responsetotal
    measured81.2 ms84.5 ms82.0 ms248 ms
    5 requests: 1,240 ms over 5 connections. Time over RTT counts 15 round trips; the model says 15. Reusing one connection takes 582 ms.

    Measured on loopback through a proxy that delays each direction by half the RTT. At 1 ms, CPU time is a large share of each phase.

    Each handshake adds whole round trips. Reuse removes them from every request after the first, and the saving grows with the RTT.

    G

    Head-of-line blocking

    recorded: 6 requests, warm connections
    First response takes 100 ms on the serverHTTP/1.1, 1 conn5 heldHTTP/1.1, 6 conns0 heldHTTP/2, 1 conn0 heldOne segment lost, resent after 200 msHTTP/1.1, 1 conn6 heldHTTP/1.1, 6 conns1 heldHTTP/2, 1 conn6 held050100150200250300350ms after the burst · RTT 20 ms · ring: the slow request
    layoutslow responselost segment
    HTTP/1.1, 1 connectionAll waitAll wait
    HTTP/1.1, 6 connectionsOnly the slow one1 of 6 waits
    HTTP/2, 1 connectionOnly the slow oneAll wait
    HTTP/3 (QUIC)Only the slow oneIts stream waits

    In the lab, the last fast HTTP/1.1 request finished at 238 ms. With HTTP/2 and a lost segment, every response took over 200 ms. HTTP/3: derived from RFC 9000. The 200 ms resend matches the Linux minimum retransmission timeout.

    HTTP/2 fixes head-of-line blocking at the HTTP layer, but TCP still delivers bytes in order. One lost segment holds every stream on the connection, which QUIC avoids.

    H

    Server push to the client

    WebSocket, SSE, long polling
    methodhowstatus
    PollingAsk every N seconds; most answers are empty.Wasteful
    Long pollingThe server holds the request until data arrives. One message per response.Fallback
    Server-sent eventsOne HTTP response stays open. Server to client only; the browser reconnects.Feeds
    WebSocketAn HTTP upgrade, then frames both ways on one connection.Two-way
    gRPC streamingHTTP/2 streams: server, client or both directions.Between services

    Over HTTP/1.1, browsers limit open connections per host, so many SSE tabs can block. Over HTTP/2, each stream shares one connection.

    For server-to-client updates I use server-sent events. I use WebSocket when the client also sends often, and long polling only where neither works.

    I

    Count the round trips

    pseudo code
    round trips per requestpseudo code
    setup(protocol):                      // round trips before the request leaves
      TCP + TLS 1.2:   1 + 21
      TCP + TLS 1.3:   1 + 12
      QUIC (HTTP/3):   0 + 1              // transport and TLS in one3
      QUIC with 0-RTT: 0 + 0              // request in the first packet4
    
    round_trips(protocol, n, reuse):
      IF reuse: RETURN setup + n          // pay the setup once5
      RETURN n × (setup + 1)              // pay it on every request
    1. 1TLS 1.2: hello and key exchange, then the Finished messages.
    2. 2TLS 1.3 sends the key share in the first hello. HTTP/2 adds nothing: ALPN picks it inside the handshake.
    3. 3QUIC carries the TLS 1.3 handshake in its own first packets.
    4. 4Uses a key from an earlier session. An attacker can replay this packet.
    5. 5The lab checks every phase of every request against this model at an 80 ms RTT.
    Tested source Go: the model
    Go: the modelgo
    
    // Setup is the round trips a new connection costs before its first request can leave:
    // the TCP handshake, then the TLS handshake.
    func Setup(p Proto) (tcp, tls int) {
      switch p {
      case TCP, HTTP1:
        return 1, 0 // SYN, SYN-ACK
      case TLS12:
        return 1, 2 // hello and key exchange, then Finished both ways
      case TLS13, H2:
        return 1, 1 // the client sends its key share in the first hello
      case H3:
        return 0, 1 // QUIC runs the transport and TLS 1.3 handshakes together
      case H30RTT:
        return 0, 0 // the request rides in the first packet, with a resumed key
      }
      panic(fmt.Sprintf("unknown protocol %q", p))
    }
    
    // RoundTrips is the total for n requests sent one after another.
    func RoundTrips(p Proto, n int, reuse bool) int {
      tcp, tls := Setup(p)
      if reuse {
        return tcp + tls + n // one setup, then 1 round trip per request
      }
      return n * (tcp + tls + 1) // every request pays the setup again
    }
    

    My latency estimate is setup round trips plus one per request, times the RTT. Reuse pays the setup once.

    J

    Payload size and slow start

    pseudo code, then recorded sizes
    round trips to deliver a bodypseudo code
    transfer_round_trips(bytes):          // new connection, no loss
      segments = ceil(bytes / 1460)       // MTU 1500 - 40 bytes of headers1
      window = 10                         // initial window, segments2
      rounds = 0
      WHILE segments > 0:
        segments -= window
        window = window × 2               // slow start doubles it3
        rounds += 1
      RETURN rounds
    1. 1Ethernet MTU 1,500 minus a 20-byte IPv4 header and a 20-byte TCP header.
    2. 2RFC 6928: 10 segments, about 14.6 KB.
    3. 3With no loss, the window doubles each round trip until a threshold.
    Tested source Go: slow start
    Go: slow startgo
    
    // TransferRounds is the round trips a new connection needs to deliver n bytes in slow start,
    // with no loss: the window starts at 10 segments and doubles every round trip.
    func TransferRounds(n int) int {
      segs := (n + MSS - 1) / MSS
      rounds, cwnd := 0, InitCwnd
      for segs > 0 {
        segs -= cwnd
        cwnd *= 2
        rounds++
      }
      return max(rounds, 1)
    }
    
    JSON linesraw KBgzip KBround trips, rawgzip
    101.40.411
    100142.611
    1,000139.82242
    10,0001,398214.174

    A new connection sends 10 segments, about 14 KB, in its first round trip, then doubles. Compression cuts round trips, not only bytes.

    K

    Capabilities used

    what each tool gives you
    toolcapabilitywhat it gives this designalso used for
    ServiceHTTP client pool with keep-aliveOne setup, then 1 round trip per request. No handshake CPU per call.Calls to payment and search providers
    ServiceHTTP/2 or gRPC: many streams on one connectionConcurrent calls without one connection each; no HTTP head-of-line blocking.Service-to-service calls, streaming
    Servicegzip or Brotli on text bodiesFewer segments, so fewer slow-start round trips on new connections.APIs, HTML, logs
    PostgresConnection pool in the service, or a pooler such as PgBouncerEach Postgres connection is a server process, with TCP, TLS and authentication. A pool pays that once.Limits on total connections
    PostgresPipeline mode in the client librarySend several queries without waiting for each result: fewer round trips.Bulk loads
    RedisPipeliningMany commands in one round trip.Batch reads, cache warm-up
    DNSTTL on each recordResolvers cache the answer, so most requests skip the lookup. A short TTL makes a change take effect fast.Failover, traffic steering
    Load balancerTLS termination, keep-alive to backendsHandshakes happen once at the edge of the network; backends get warm connections.Certificates in one place
    CDN edgePoints of presence near usersThe handshake round trips are short; the long link to the origin stays warm.Caching, DDoS absorption
    QUIC, HTTP/31-RTT setup, 0-RTT resumption, independent streamsOne round trip less on new connections; a loss holds only its stream.Mobile clients on lossy networks

    I keep connections warm at every hop. The service pools its connections, the balancer keeps them alive, and TLS ends at an edge near the user.

    L

    Failure cases

    what breaks, and what saves you
    eventresultwhy it stays correctsaved by
    A deploy drops every connection, and all clients reconnect at onceA connection storm: each handshake costs about 12 times a warm request in CPU.Clients reconnect after a random delay, so the handshakes spread over time.Jittered backoff
    A client opens and closes a connection per callClosed sockets hold ports in TIME_WAIT. The lab's loop used all 16,384 ports in seconds.A pool keeps a few connections open and reuses them.Connection pool
    A failover changes an IP, but the DNS TTL is 1 dayClients keep the old address until their cached record expires.Lower the TTL one old TTL ahead, or fail over behind an address that does not change.DNS TTL
    One slow response on an HTTP/1.1 connectionEvery request queued behind it waits.HTTP/2 streams let the fast responses pass.HTTP/2
    One lost segment on an HTTP/2 connectionAll streams wait for the resend.QUIC delivers each stream in order on its own.HTTP/3
    The load balancer closes idle connections firstThe next request on that pooled connection fails.Set the pool's idle timeout below the balancer's. Retry an idempotent request once.Idle timeout
    A path drops large packets and blocks ICMPSmall requests work; large responses stall.Clamp the TCP MSS at the tunnel, or let the host probe the path MTU.MSS clamping
    M

    Scale ladder

    start simple; climb only on a signal
    Each step adds one component1Keep-alive2+ LB ends TLS3+ HTTP/2, gRPC4+ edge near users5+ HTTP/3more load →
    Setup cost against demand, one 8-core machine1001k10k100kStorm: 100k clients in 10 s: 10,000 per secondStorm: 100k clients in 10 s10,000TLS 1.3 handshake + 1 request: 2,076 per secondTLS 1.3 handshake + 1 request2,076TLS 1.2 handshake + 1 request: 2,647 per secondTLS 1.2 handshake + 1 request2,647Request on a warm HTTP/2: 20,758 per secondRequest on a warm HTTP/220,758Request on a warm HTTP/1.1: 26,268 per secondRequest on a warm HTTP/1.126,268per second, log scale
    stepaddit handlesmove up when you see
    1One server with keep-alive, and pools to the database and to other services.About 26,300 requests a second on warm connections in the lab, against about 2,100 with a new TLS connection each.One server is not enough, or certificates and handshakes spread over many hosts.
    2A load balancer that ends TLS and keeps warm connections to the servers.Handshakes in one tier; servers scale behind it.Many service-to-service calls, each holding its own HTTP/1.1 connection.
    3HTTP/2 or gRPC between services.Many concurrent calls on few connections; streaming calls.Users far away: three long round trips before the first byte.
    4Edges near users, with warm links to the origin.Short handshakes everywhere; one long round trip per request.Mobile users on lossy networks; streams held by one lost segment.
    5HTTP/3 at the edge, with HTTP/2 as the fallback.1-RTT setup, 0-RTT for idempotent requests, no TCP head-of-line blocking.Top of the ladder.

    Rates: medians of three runs on an 8-core laptop, 8 clients, client and server on the same machine, so each handshake pays both sides' CPU. Runs varied by up to 1.6 times. The storm is an example: 100,000 clients that reconnect within 10 s.

    I start with one server and keep-alive. I add a load balancer that ends TLS, then HTTP/2 between services, then edges near users, and HTTP/3 last.

    N

    Drill

    predict, then reveal

    0 of 10 known

    1. How many round trips does a new HTTPS connection take before the first response byte, with TLS 1.3 and with TLS 1.2?

    2. Why does connection reuse save more at 80 ms than at 1 ms?

    3. What is head-of-line blocking in HTTP/1.1, and how does HTTP/2 remove it?

    4. When can HTTP/2 on one connection be slower than six HTTP/1.1 connections?

    5. What does QUIC change?

    6. What is the risk of 0-RTT data?

    7. You lower a DNS TTL from 1 day to 60 s just before a failover. Why do clients still go to the old address?

    8. A service opens a new connection for every call to another service, 2,000 calls a second. What breaks first?

    9. A live score feed only sends from server to client. WebSocket or server-sent events?

    10. Why gzip a 139.8 KB JSON response when bandwidth is plentiful?

    O

    Numbers to say

    measured, derived or cited
    fiber
    About 200 km per ms one way: 1 ms of RTT per 100 km, at best.
    ocean
    Frankfurt to Virginia: RTT of at least 66 ms.
    setup
    New HTTPS: 3 RTT with TLS 1.3, 4 with TLS 1.2, 2 with HTTP/3. Reused: 1.
    at 80 ms
    One request: 248 ms new, 81 ms reused.
    CPU
    About 2,100 new TLS connections a second against 26,300 warm requests: about 12 times.
    window
    10 segments of 1,460 bytes in the first round trip: 14.6 KB.
    ports
    macOS: 16,384 ephemeral ports, 30 s in TIME_WAIT, so about 546 new connections a second to one address. Linux: 28,232 ports, 60 s, about 470.

    Cited: RFC 6928, RFC 8446, RFC 9000, RFC 9001, and the operating system defaults. Measured: Go servers on an 8-core laptop.