awesome-everything RU
↑ Back to the climb

Backend Architecture

Accept and parse: from kernel queue to a typed request

Crux Before your code runs, the kernel completes the handshake into a bounded accept queue, and the server turns raw bytes into a request line, headers, and body — each step with a hard limit you can overflow.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 14 min

A service passes every load test, then refuses connections in production during a traffic spike — not with a 500, but with a TCP reset before any of your code runs. The logs are empty because the request never reached the application. The bottleneck was a kernel queue with a default depth of 128, set in 1999 and never changed.

Two queues, not one

A listen() socket has two kernel-side queues, and confusing them causes most “connections refused under load” mysteries:

  • SYN queue (incomplete) — connections mid-handshake: the server got a SYN, sent SYN-ACK, and is waiting for the final ACK. Sized by net.ipv4.tcp_max_syn_backlog (commonly ~1024–2048).
  • Accept queue (complete) — fully established connections waiting for the application to call accept(). Sized by min(listen(backlog), net.core.somaxconn).

When your handler is slow or your process is busy, it stops calling accept() fast enough. The accept queue fills. Once full, the kernel drops new completed connections — the client sees a timeout or reset, and your application logs nothing because the connection was never handed up.

The 128 trap

net.core.somaxconn defaulted to 128 on Linux for two decades (raised to 4096 in kernel 5.4). Even today many container base images and managed platforms still ship 128. A framework that calls listen(511) (Node’s default backlog) is silently capped to 128 by somaxconn. Under a burst, 129 simultaneous pending connections is enough to start dropping.

SettingCommon defaultWhat it boundsOverflow symptom
tcp_max_syn_backlog~1024–2048Half-open handshakesSYN flood / dropped SYNs
somaxconn128 (older), 4096 (5.4+)Established, awaiting accept()Connection refused / reset, no app log
listen(backlog)511 (Node), 511 (nginx)App’s requested accept depthCapped down to somaxconn

Parsing: bytes to a request

Once accept() returns a socket, the server reads bytes and runs an HTTP parser — a state machine that recognizes the request line (GET /path HTTP/1.1), then header lines, then the body. Parsing is not free of limits:

  • Header size cap. Node.js limits total headers to 16 KB (--max-http-header-size, raised from 8 KB in Node 12). nginx uses large_client_header_buffers (default 4 8k). Exceed it and you get 431 Request Header Fields Too Large or a 400 — most often caused by a bloated Cookie header or a large JWT.
  • Request line length. A URL longer than the buffer (nginx 8k by default) is rejected before routing.

Where the body comes from

The parser learns the body length one of two ways, and they fail differently:

  • Content-Length: N — read exactly N bytes. A lying or truncated length causes a hang or a parse error.
  • Transfer-Encoding: chunked — read sized chunks until a zero-length chunk. Streaming-friendly, but the classic vector for request smuggling when a proxy and an app server disagree about which header wins.

A safe parser rejects a request that carries both Content-Length and Transfer-Encoding (RFC 9112 §6.1) — disagreement here is exactly how a smuggled request hides a second request inside the first.

Quiz

Under a traffic burst, clients get connection timeouts but the application logs are completely empty. What is the most likely cause?

Quiz

A request fails with 431 Request Header Fields Too Large only for logged-in users. What is the likely cause?

Quiz

Why should an HTTP parser reject a request that contains BOTH Content-Length and Transfer-Encoding: chunked?

Order the steps

Order the steps from packet arrival to a parsed request:

  1. 1 Client SYN arrives; connection enters the SYN queue (half-open)
  2. 2 Handshake completes; connection moves to the accept queue
  3. 3 Application calls accept(); kernel hands up the connected socket
  4. 4 Server reads bytes and parses the request line (method, path, version)
  5. 5 Parser reads header lines until the blank line, enforcing the size cap
  6. 6 Body is read by Content-Length or chunked framing
Recall before you leave
  1. 01
    What are the two kernel queues behind a listen() socket, and what bounds each?
  2. 02
    Why is somaxconn = 128 a production trap, and how does it interact with the framework's backlog?
  3. 03
    How does the parser determine body length, and why is presence of both Content-Length and Transfer-Encoding dangerous?
Recap

Before any application code runs, the kernel manages two queues per listen socket: the SYN queue for half-open handshakes (tcp_max_syn_backlog) and the accept queue for established connections awaiting accept() (min of the framework backlog and somaxconn). The accept queue is the silent killer — its default of 128 is easy to overflow under burst, and overflow drops connections with no application log. Once accept() returns, an HTTP parser turns bytes into a request line, headers (capped at 16 KB in Node, configurable in nginx via large_client_header_buffers), and a body framed by Content-Length or chunked encoding. Oversized headers yield 431/400; both framings at once is the request-smuggling vector RFC 9112 forbids. With a parsed request in hand, the next stop is choosing what code runs: routing and the middleware chain.

Connected lessons
appears again in185
Continue the climb ↑Routing and middleware: choosing what runs, and in what order
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.