In HTTP/2, what do stream identifiers signify, and how do the RST_STREAM and GOAWAY frames let a server shut a connection down without discarding requests that are already being processed?
answer
- odd=client, even=server, 0=connection, never reused
- RST_STREAM = one stream; GOAWAY = whole connection
- GOAWAY carries last-processed stream ID + error code
- above the cutoff = not processed = safe retry
- two GOAWAYs: 2^31-1, PING, then the real ID
basics
~20 sStream IDs are monotonically increasing: odd from the client, even from the server, 0 for connection frames. RST_STREAM kills one stream. GOAWAY names the highest stream the server will process, so anything above it is untouched and safely retryable. Graceful shutdown sends GOAWAY twice.
solid answer
~50 sStream IDs increase monotonically and are never reused, which is what makes GOAWAY work: the frame carries a **last-stream-ID** plus an error code, and it means "I processed nothing above this". Streams above the cutoff can be retried elsewhere without duplicating side effects; streams at or below it are allowed to finish. **RST_STREAM** terminates a single stream with an error code (CANCEL, REFUSED_STREAM, NO_ERROR) and leaves the connection intact. **GOAWAY** is connection-level and is not an immediate close. The graceful-drain pattern from RFC 9113 is two GOAWAYs: first send GOAWAY with last-stream-ID `2^31-1` and NO_ERROR — a "stop sending me new work, but I have not cut anything off" signal — then send PING and wait a round trip so in-flight HEADERS arrive, then send a second GOAWAY with the real highest processed stream ID, let those finish, and close. Without the first GOAWAY you race the client and force retries.
code
http · 7 linesS->C GOAWAY last_stream_id=2147483647 error=NO_ERROR
S->C PING opaque=0x0102030405060708
C->S HEADERS stream=101 POST /orders (already in flight)
C->S PING ACK opaque=0x0102030405060708
S->C GOAWAY last_stream_id=101 error=NO_ERROR
... streams <= 101 complete ...
S->C [TCP FIN]go deeper
Know that stream IDs count upward, RST_STREAM cancels one request, and GOAWAY says the connection is winding down.
Explain the last-stream-ID promise and which streams are safe to retry, plus the common error codes.
Walk the two-GOAWAY drain with the PING round trip, tie it to zero-downtime deploys, and mention reset-rate abuse.
Discuss connection-age policy as a load-balancing lever, retry budgets for the replayed streams, and the client-side contract needed for a drain to be truly invisible.
## Stream identifiers A stream ID is a 31-bit number in every frame header. Rules worth memorizing: - **Stream 0** is the connection itself: SETTINGS, PING, GOAWAY, and connection-level WINDOW_UPDATE live there. - **Client-initiated** streams are odd (1, 3, 5, …); **server-initiated** are even. - IDs must strictly increase for each initiator, and are never reused. Opening stream 7 implicitly moves streams 3 and 5 from idle to closed if they were never used. - Because the space is finite (2^31-1), a very long-lived, busy connection eventually exhausts IDs and must be replaced — a real consideration for gRPC services that hold connections for weeks. The monotonic ordering is the property that shutdown depends on: "everything above ID N" is a well-defined set of requests. ## RST_STREAM RST_STREAM aborts one stream immediately in both directions and carries a 32-bit error code. Common codes: - `CANCEL` — the sender no longer wants the response (browser navigated away, client-side deadline expired, gRPC cancellation). - `REFUSED_STREAM` — the receiver did not process the request at all, so retry is safe even for POST. - `NO_ERROR` — used, for example, when a client has read enough of a response. After sending RST_STREAM, an endpoint may still receive frames for that stream for up to a round trip and must ignore them rather than error. The rest of the connection continues normally — this is the whole point of a per-stream reset. The abuse case is worth knowing: HTTP/2 Rapid Reset (CVE-2023-44487) sent HEADERS immediately followed by RST_STREAM in a loop, so the open-stream count stayed under `SETTINGS_MAX_CONCURRENT_STREAMS` while backend work piled up. Modern servers count recently-reset streams and shut the connection with GOAWAY / `ENHANCE_YOUR_CALM` when the reset rate is abusive. ## GOAWAY GOAWAY is sent on stream 0 and carries: - the **last stream ID** the sender processed or might process, - an **error code**, - optional opaque debug data. Its semantics are a promise, not an action: streams with IDs **greater than** the last-stream-ID were not processed, so the peer can replay them on a different connection with no risk of duplicate side effects. Streams at or below it may still be in progress and should be allowed to complete. The connection is still usable for those streams until the sender actually closes the TCP connection. An `error_code` of `NO_ERROR` means an orderly shutdown (deploy, idle timeout, connection age limit); anything else (`PROTOCOL_ERROR`, `ENHANCE_YOUR_CALM`, `INTERNAL_ERROR`) reports a fault. ## The two-GOAWAY graceful drain The race is this: the moment a server picks a cutoff ID, the client may already have new HEADERS in flight below that ID's successor. RFC 9113 §6.8 describes the fix: 1. Send `GOAWAY(last_stream_id = 2^31-1, NO_ERROR)`. This tells the client to stop opening new streams but cuts nothing off. 2. Send a `PING` and wait for its ACK (one round trip), so any HEADERS the client had already dispatched arrive. 3. Send a second `GOAWAY` with the true highest processed stream ID. 4. Let those streams finish, optionally with a deadline, then close the TCP connection. Go's `http2.Server` (and therefore `Server.Shutdown`), Envoy's drain sequence, and gRPC's `GracefulStop` all implement this shape. Skipping step 1 turns every deploy into a burst of client retries and, for non-idempotent requests that clients refuse to retry, into user-visible errors. ## Client responsibilities On receiving GOAWAY a well-behaved client must: stop creating streams on that connection, open a fresh connection for new work, retry any stream above the last-stream-ID, and let the remaining streams complete. Common bugs are treating GOAWAY as an immediate connection close (aborting in-flight requests unnecessarily) and retrying streams *below* the cutoff, which can duplicate a payment. ## Operational uses Beyond deploys, servers send GOAWAY to enforce maximum connection age or maximum idle time so that clients periodically re-resolve DNS and rebalance across backends — otherwise a long-lived HTTP/2 connection pins a client to one server instance indefinitely. gRPC exposes exactly this as `MAX_CONNECTION_AGE` with a grace period, and it is the standard cure for load imbalance after a scale-up.
- A client receives GOAWAY with last_stream_id=41 while it has streams 39, 41 and 43 open. What should it do with each?Streams 39 and 41 are at or below the cutoff, so they were or may have been processed — let them run to completion and use their responses. Stream 43 is above the cutoff and is guaranteed unprocessed, so retry it on a new connection, even if it was a POST. New requests must go to a fresh connection.
- Why do servers send GOAWAY on a timer even when nothing is wrong?An HTTP/2 connection is long-lived and pins a client to one backend instance, so newly added instances get no traffic and DNS changes are never picked up. Periodically ageing connections out with a graceful GOAWAY (gRPC's MAX_CONNECTION_AGE plus grace period) forces clients to re-resolve and rebalance, at the cost of an occasional handshake.
GOAWAY is a shop announcing "last orders": customers already served finish their meal, anyone who ordered after the announcement is told to go elsewhere, and the doors close only afterwards.
saying these in an interview costs you the question
- Treating GOAWAY as an immediate close and aborting in-flight streams
- Retrying streams at or below the last-stream-ID, risking duplicate side effects
- Sending one GOAWAY with the current stream ID and calling it graceful, which races requests already in flight
- Assuming RST_STREAM tears down the connection rather than a single stream
- Thinking MAX_CONCURRENT_STREAMS alone bounds work, ignoring rapid open-and-reset abuse