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.
- 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.
- 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:
- "What does
listendo?" — the warm-up about a socket going passive. - "What is the backlog?" — where the substance starts: the length of a queue of what, exactly.
- "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.
- "What does a client see when the queue overflows?" — a trap: not a refusal.
- "Why are the server's logs empty while clients report timeouts?" — a question about the queue leaving no trace inside the application.
- "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:
- it creates a socket — a point through which it can speak over the network;
- it binds that socket to an address and a port — now there is somewhere to knock;
- it declares the socket listening — the socket goes passive: it will never connect anywhere itself, it will accept;
- 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
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.
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
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.
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.
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
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.
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
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.
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
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
Why did the client time out if the server is alive and answering?
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.
Does raising the backlog help?
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.
How do you see the queue without changing the application?
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.
What does tcp_abort_on_overflow give you?
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
until the server calls accept there is no connection
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.
on overflow the client gets ECONNREFUSED
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.
backlog=1 means one connection in the queue
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.
there is one queue
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.
we set backlog=1000, so the queue holds a thousand
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.
if clients time out while the server answers its health check, the network is at fault
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.
a longer queue is always better than a short one
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
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
Knowledge check
A server called listen but never calls accept. A client connects. What happens?
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- 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 -lntshowsSend-Qas the capacity andRecv-Qas the current length. The number inlisten(backlog=…)is silently capped tosomaxconn— measured: withsomaxconn=2the queue came out at 2 while the code said 100. And on this kernel one more connection fits into the queue thanbacklogsays.
In fact
- There is: the handshake is done by the kernel, and
acceptonly extracts a ready connection from the queue — "It extracts the first connection request on the queue of pending connections". Measured: a server that never calledaccept, a client connected in 0.05 ms and 18 bytes sent. - 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_overflowenabled. - Measured: two. With
backlog=8it is nine: on this kernel one more connection gets into the queue thanbacklognames. But "plus one" must not become a rule either —listen(2)promises only the maximum to which the queue may grow. Practical consequence:backlogis an order of magnitude, and the actual capacity is read fromss. - There are two. The first holds connections whose handshake is unfinished (sized by
tcp_max_syn_backlog), the second holds finished ones waiting foraccept(sized bybacklog). The first overflows from a flood of requests, the second from a slow application, and the cures differ. - Only if
somaxconnis 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: withsomaxconn=2the samelisten(backlog=100)produced a queue of 2. And there is no error —listenreturned success. - 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-Qon the listening socket: the queue belongs to the kernel and is visible from outside. - 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.
What is covered
- What is actually being asked
- Base: what a server does while nobody is there
- Mechanism 1: the kernel establishes the connection
- Mechanism 2: there are two queues, and they overflow differently
- Mechanism 3: an overflow looks like a timeout, not like a refusal
- Deeper: the backlog in your code is not the queue you get
- How to answer in an interview
- Next they ask
- Common misconceptions
- Practice
- Knowledge check
Sources & further reading
3 SOURCES
- 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
- 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
- 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