Deep Engineering
Intermediate·Published·20 MIN

The accept queue: the connection is established and the server knows nothing about it

A client connected, sent its request and is waiting. The server has not called `accept` once and has no idea the client exists. Between them sits a kernel queue — and most of what looks like "the network is slow" actually happens in it.

Full technical treatment

TL;DR

Between "the client connected" and "the server learned about the client" there is a queue. The kernel establishes the connection; the application later takes the ready one with accept. Until it does, the client is connected and the server knows nothing about it.

Hence the main consequence: a busy server looks like a working one. Measured: a server that never called accept — the client connected in 0.05 ms and sent 18 bytes. And on overflow the client gets waiting rather than a refusal: the packet is dropped silently, TCP retries it, and the server's logs show nothing.

Beyond that is what separates knowing from having read. There are two queues, and they overflow for different reasons. The queue is observable from outside: for a listening socket ss -lnt shows Send-Q as the capacity and Recv-Q as the current length. The number in listen(backlog=…) is silently capped to somaxconn — measured: with somaxconn=2 the queue came out at 2 while the code said 100. And on this kernel one more connection fits into the queue than backlog says.

Where to start
Before this lesson it is enough to understand
  • a client sends a request to a server and the server answers;
  • a TCP connection is established before any data travels over it;
  • a server opens a listening socket and somehow gets incoming connections out of it.
You do not need to know in advance
  • how the TCP handshake works packet by packet, what a SYN is, what a half-open connection is;
  • backlog, somaxconn, Recv-Q, tcp_abort_on_overflow.

What is actually being asked

The ladder usually runs like this:

  1. "What does listen do?" — the warm-up about a socket going passive.
  2. "What is the backlog?" — where the substance starts: the length of a queue of what, exactly.
  3. "A client connected and the server is busy. Where is the request?" — the question that shows whether the candidate knows about the kernel's queue.
  4. "What does a client see when the queue overflows?" — a trap: not a refusal.
  5. "Why are the server's logs empty while clients report timeouts?" — a question about the queue leaving no trace inside the application.
  6. "We set the backlog to a thousand — why did it not help?" — a question about somaxconn.

The numbers come from running bench/acceptq/backlog.py and bench/acceptq/practice.py over loopback. There is almost no timing here: connections are counted and kernel counters are read: your milliseconds will be your own, the connection counts and the outcomes the same.

Base: what a server does while nobody is there

Before talking about the queue it is worth naming, in ordinary words, what actually happens — because the queue appears exactly between two steps of that list.

A server waiting for clients does four things in order:

  1. it creates a socket — a point through which it can speak over the network;
  2. it binds that socket to an address and a port — now there is somewhere to knock;
  3. it declares the socket listening — the socket goes passive: it will never connect anywhere itself, it will accept;
  4. it accepts connections — one at a time, in a loop, each time getting a separate socket for talking to that one client.

The fourth step is the key one. The listening socket does not turn into a connection: it stays where it is and serves as the source from which the server takes ready connections, each with its own separate socket.

And here is the question this lesson is about: what happens between the third step and the fourth? A client can knock at any moment — including while the server is busy with the previous client and has not reached the fourth step yet.

The answer: agreeing on a connection is the kernel's work, not the application's. The kernel does it by itself, puts the ready connection in a queue and waits for the application to come and take it. The application learns about the client not when the client connected, but when it got round to the fourth step itself.

That is already enough to answer the basic interview question. Everything below is about that queue being finite, about there being two of them, and about what happens when a queue runs out.

Mechanism 1: the kernel establishes the connection

language contractBehaviour described in listen(2), accept(2) and tcp(7). It does not depend on the language or the framework: the queue belongs to the kernel.

Now the same thing in the documentation's words. Start with what accept does — and what it does not:

It extracts the first connection request on the queue of pending connections for the listening socket, sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket.

accept(2)

Extracts from the queue. So by the time of the call the connection already exists: the kernel completed the handshake while the application was busy with something else. The application does not establish a connection — it picks up an established one.

Check it with an extreme case: a server that never calls accept at all.

1. THE SERVER NEVER CALLS ACCEPT, AND THE CLIENT CONNECTS ANYWAY
----------------------------------------------------------------
  server called accept                         no, not once
  client's connect() returned                  success in 0.05 ms
  bytes the client managed to send             18
  listening socket in ss                       Recv-Q=1 Send-Q=5
measured observationbench/acceptq/backlog.py, Linux 6.18.44, loopback. Your milliseconds will differ; the outcome will not: the connection is established, the data is taken, the application knows nothing.

The ss line describes the socket opened in this block with listen(backlog=5): Send-Q is the queue's capacity — the backlog itself — and Recv-Q=1 is the single connection standing in it.

The client connected. The client sent eighteen bytes and they were taken. The client is convinced it is talking to a server — and the server does not know it exists and will not until it calls accept.

Hence the lesson's central consequence: from the client's side "the server is busy" and "the server is working" look the same, right up to the moment its own response timeout runs out. All that time the request sits in a queue, and no application log has it.

Mechanism 2: there are two queues, and they overflow differently

The second thing asked: how many queues are there really?

The queue of unacknowledged requests holds connections whose handshake is unfinished: the first packet arrived, the answer was sent, the client's acknowledgement has not come. Its size is set separately, and tcp(7) describes it like this:

The maximum number of queued connection requests which have still not received an acknowledgement from the connecting client. If this number is exceeded, the kernel will begin dropping requests.

tcp(7), tcp_max_syn_backlog

The queue of established connections is the one accept takes from. Its size is set by backlog:

The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow.

listen(2)

The difference is practical. The first queue overflows when there are too many clients or when a stream of half-open connections is aimed at you. The second overflows when the application is not calling accept fast enough — from its own slowness, not from the network.

How much the second one holds is settled by counting:

2. HOW MANY FIT: BACKLOG 1 AGAINST BACKLOG 8
--------------------------------------------
  listen(backlog=1): connections accepted by the kernel 2
    queue as ss sees it                        Recv-Q=2 Send-Q=1
    what stopped the loop                      timeout: the client waits, nobody refused it
  listen(backlog=8): connections accepted by the kernel 9
    queue as ss sees it                        Recv-Q=9 Send-Q=8
    what stopped the loop                      timeout: the client waits, nobody refused it
measured observationbench/acceptq/backlog.py. Read the ss lines like this: for a listening socket Send-Q is the queue's capacity and Recv-Q is how many are in it right now.

Plus one. With backlog=1 two connections fit, with backlog=8 nine. That is not a measurement error: on this kernel one more connection gets into the queue than backlog names.

And here is where an observation must not be turned into a rule. listen(2) calls backlog the maximum length to which the queue may grow and promises no exact correspondence to the number; POSIX treats it as a hint the implementation is free to interpret its own way. "Plus one" is a property of this version of Linux, not a contract: on another kernel the arithmetic may differ, and code that counts on exactly backlog + 1 breaks silently.

The right conclusion runs the other way: backlog is an order of magnitude, not an exact capacity. If you need to know the queue's actual length, you do not compute it from the code — you look at it, which is what comes next.

And note Recv-Q/Send-Q in the ss output: for a listening socket they mean something other than usual. Send-Q is the queue's capacity, Recv-Q is its current length. That is how the queue is seen from outside without touching the application.

Mechanism 3: an overflow looks like a timeout, not like a refusal

Now the trap the whole lesson exists for. What does a client see when the queue is full?

If a connection request arrives when the queue is full, the client may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt at connection succeeds.

listen(2)

TCP does support retransmission — so the second branch applies:

3. WHAT OVERFLOW LOOKS LIKE FROM THE CLIENT
-------------------------------------------
  connections before the loop stopped          2
  how it ended                                 timeout: the client waits, nobody refused it
  tcp_abort_on_overflow                        0
measured observationbench/acceptq/backlog.py with backlog=1 and the default tcp_abort_on_overflow. The client waits out its own timeout; it never gets a refusal.

Not a single ECONNREFUSED. The client waits, because its packet was silently dropped and TCP rules say it will try again. To an observer this looks like:

  • the client reports a connection timeout and blames the network;
  • the server's logs hold nothing: the application never learned of the attempt;
  • the server's metrics show it alive and serving whatever it did pick up.

The behaviour can be changed — tcp(7) describes tcp_abort_on_overflow as the switch that makes the kernel reset such connections instead of staying silent. Then the client gets an explicit refusal instead of waiting. The documentation immediately warns to enable it only when you are sure of the cause: an explicit refusal breaks clients that would have retried and got through.

Deeper: the backlog in your code is not the queue you get

The last step explains why "we set it to a thousand" does not work:

If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently capped to that value. Since Linux 5.4, the default in this file is 4096; in earlier kernels, the default value is 128.

listen(2)

The word silently is the key one: no error, no warning, listen returns success. Check it by lowering somaxconn to two:

4. THE BACKLOG IN YOUR CODE IS NOT THE QUEUE YOU GET
----------------------------------------------------
  somaxconn on this machine                    4096
  listen(backlog=100) with that somaxconn      Recv-Q=0 Send-Q=100
  same listen(backlog=100), somaxconn=2        Recv-Q=0 Send-Q=2
  connections the kernel took                  3
  somaxconn restored                           4096
measured observationbench/acceptq/backlog.py. The same listen(backlog=100) line, two machine settings — and a different queue capacity.

The same code on two machines: a queue of a hundred and a queue of two. Hence the practical rule: check ss, not the code. A listening socket's Send-Q shows what you got, not what you asked for.

The second consequence concerns rolling out into a new environment. The application's setting and the kernel's are linked, and the link runs one way: the kernel constrains the application silently. So the post-deployment check includes not only "the service answers" but "the queue is as long as we asked for".

How to answer in an interview

Short answer: between the client's connect and the server's accept sits a kernel queue, and a connection in it is already established — the kernel did the agreeing, not the application. That is why a busy server looks like a working one to a client, and why an overflowing queue gives the client a timeout rather than a refusal: the packet is dropped silently and TCP retries it.

That is enough for a correct answer. What follows is what you add when the interviewer digs.

If the interviewer digs deeper

Three things separate a good answer. First, you talk about two queues and distinguish their causes: unacknowledged requests overflow because of traffic, established ones because of a slow application. Second, you name what is observable: Recv-Q and Send-Q on a listening socket, and what they mean there specifically. Third, you remember somaxconn and that the capping is silent: the number in the code has to be verified against reality.

And one thing that is easy to overdo. Saying "the queue holds backlog + 1" passes an observation off as a contract. listen(2) promises only the maximum to which the queue may grow; "plus one" came out on one particular kernel. The precise phrasing is this: the actual capacity is not derived from the code, it is read from ss — which is also the right answer to how you would check the queue in production.

Next they ask

Next they ask

Why did the client time out if the server is alive and answering?

Short answer

Because "the server answers" and "the server takes connections out of the queue" are different things. While the application is busy, the connection sits in the established queue; the client is already connected and has already sent its request, and there will be no answer until somebody calls accept.

Measured: a server that never called accept took a connection in 0.05 ms and eighteen bytes of data. So a "the server answers its health check" metric will not catch this state — a queue-length metric will.

Next they ask

Does raising the backlog help?

Short answer

It helps survive a burst: a queue exists precisely so that a short spike does not turn into refusals. It does not help against sustained overload — if the application takes connections more slowly than they arrive on average, a longer queue only lengthens the wait before overflowing anyway.

The distinguishing symptom is simple: a queue that fills and drains means the headroom is working; a queue that stays full means the problem is elsewhere. The second case is fixed by processing speed or by the number of workers.

Next they ask

How do you see the queue without changing the application?

Short answer

For a listening socket ss -lnt shows Recv-Q — how many connections are waiting for accept — and Send-Q — how many fit. Measured: Recv-Q=9 Send-Q=8 with listen(backlog=8).

It is the cheapest diagnosis in the topic: no code changes, no access inside the process. A rising Recv-Q on a listening socket is direct proof that the network is not the problem.

Next they ask

What does tcp_abort_on_overflow give you?

Short answer

It trades silence for an explicit refusal: instead of dropping the packet the kernel resets the connection and the client gets an error at once. Diagnosis becomes easier — the refusal is visible to both sides — but clients that would have survived the burst by retrying now fail.

The documentation frames it as a condition: enable it only if you are sure the service really cannot keep up rather than riding out a short spike.

Common misconceptions

Claim

until the server calls accept there is no connection

Actually

There is: the handshake is done by the kernel, and accept only extracts a ready connection from the queue — "It extracts the first connection request on the queue of pending connections". Measured: a server that never called accept, a client connected in 0.05 ms and 18 bytes sent.

Claim

on overflow the client gets ECONNREFUSED

Actually

Usually not. TCP supports retransmission, so the kernel drops the packet silently: the measured loop of connections ended in a timeout rather than a refusal. The client blames the network and the server's log is empty. An explicit refusal appears only with tcp_abort_on_overflow enabled.

Claim

backlog=1 means one connection in the queue

Actually

Measured: two. With backlog=8 it is nine: on this kernel one more connection gets into the queue than backlog names. But "plus one" must not become a rule either — listen(2) promises only the maximum to which the queue may grow. Practical consequence: backlog is an order of magnitude, and the actual capacity is read from ss.

Claim

there is one queue

Actually

There are two. The first holds connections whose handshake is unfinished (sized by tcp_max_syn_backlog), the second holds finished ones waiting for accept (sized by backlog). The first overflows from a flood of requests, the second from a slow application, and the cures differ.

Claim

we set backlog=1000, so the queue holds a thousand

Actually

Only if somaxconn is not smaller: "If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently capped to that value". Measured: with somaxconn=2 the same listen(backlog=100) produced a queue of 2. And there is no error — listen returned success.

Claim

if clients time out while the server answers its health check, the network is at fault

Actually

The health check answers because its connection was picked up. Connections still in the queue never reached the application, so its logs have nothing about them. The direct proof is a rising Recv-Q on the listening socket: the queue belongs to the kernel and is visible from outside.

Claim

a longer queue is always better than a short one

Actually

A long queue rides out a burst well and sustained overload badly: if the application takes connections more slowly than they arrive on average, the queue becomes an accumulator of waiting. The client waits longer and the outcome is the same. What tells them apart is the behaviour of Recv-Q: oscillating means the headroom works, permanently full means the queue is not the problem.

Practice

Two exercises. Answer first, then check against the real output: in both, the correct answer comes from a script's committed output rather than being written by hand.

Practice · predict the output

A server called listen(backlog=1) and does nothing else — accept is never called. Three things are printed: how the client's connect ended, how many bytes it managed to send, and how the loop of connections ended once the queue filled up. What does this code print?
srv, port = server(backlog=1)
outcome, sent = connect_result(port)
print(outcome)
print(sent)
srv.close()

srv, port = server(backlog=1)
filled, ending = fill(port)
print(ending)

Practice · estimate

A server called listen(backlog=1) and never takes connections. How many will the kernel accept before the next one starts waiting?
connections

Knowledge check

Question 1 of 6

A server called listen but never calls accept. A client connects. What happens?

Sources & further reading

3 SOURCES

  1. listen(2), Linux man-pages 6.7Official documentation. What the backlog sets: "The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow". What happens on overflow, and why the client does not see a refusal: "If a connection request arrives when the queue is full, the client may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt at connection succeeds". And why the number in your code may mean nothing: "If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently capped to that value. Since Linux 5.4, the default in this file is 4096; in earlier kernels, the default value is 128".https://man7.org/linux/man-pages/man2/listen.2.html
  2. tcp(7), Linux man-pages 6.7Official documentation. On the other queue — the one holding connections that are not established yet: tcp_max_syn_backlog is "The maximum number of queued connection requests which have still not received an acknowledgement from the connecting client. If this number is exceeded, the kernel will begin dropping requests". And on the setting that changes what a refusal looks like: tcp_abort_on_overflow "Enable resetting connections if the listening service is too slow and unable to keep up and accept them", with an explicit warning to enable it only when you are sure of the cause.https://man7.org/linux/man-pages/man7/tcp.7.html
  3. accept(2), Linux man-pages 6.7Official documentation. What an application actually does when it "accepts" a connection: "It extracts the first connection request on the queue of pending connections for the listening socket, sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket". By then the connection already exists — the application takes it out of a queue rather than creating it.https://man7.org/linux/man-pages/man2/accept.2.html