On Linux TCP sockets, the listen() backlog concerns established connections waiting for the application to accept them. The incomplete-handshake queue is a separate concept. Calling accept() removes a pending connection and returns a connected socket for application use. That accepted connection no longer occupies the listener’s accept queue, even if it remains open and busy for a long time.

When it breaks

If connections arrive faster than the application accepts them, the pending queue can fill. New clients may then encounter retransmission delays or timeouts, depending on kernel behavior and settings. Existing accepted requests can still look healthy. A process being alive, or some requests succeeding, does not prove that its listener is accepting new connections quickly enough.

The requested backlog is also constrained by net.core.somaxconn; increasing only a larger application value cannot exceed that cap. The exact observable queue boundary is implementation-dependent, however, so treating the integer as a portable promise of precisely that many successful simultaneous connects is unsafe. Measure the deployed kernel rather than converting one small experiment into a universal off-by-one rule.

Queue counters need context. ListenOverflows and ListenDrops are network-namespace counters, not automatically per-service explanations. Other listeners can contribute to them, and a client timeout can have causes outside the accept queue. Correlate queue occupancy, acceptance progress, and counters before attributing production failures to a particular backlog setting or declaring that a larger queue will fix them.

Check it quickly

Save this as check-listen-backlog.py and run it with host Python, Docker, and the pinned Python image available. The actual socket operations run inside a disposable Linux container with networking disabled except loopback. The recorded kernel was 6.12.76-linuxkit. The script reads relevant settings without modifying them and requires tcp_abort_on_overflow=0 for its timeout observation:

import json,subprocess
image='python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84'
program=r'''
from contextlib import ExitStack
from pathlib import Path
import json,platform,socket
def counters():
    lines=Path('/proc/net/netstat').read_text().splitlines()
    for index,line in enumerate(lines):
        if line.startswith('TcpExt:'):
            return dict(zip(line.split()[1:],map(int,lines[index+1].split()[1:])))
def queued(port):
    for line in Path('/proc/net/tcp').read_text().splitlines()[1:]:
        fields=line.split()
        if fields[3]=='0A' and int(fields[1].split(':')[1],16)==port:
            return int(fields[4].split(':')[1],16)
    raise AssertionError('listener missing')
with ExitStack() as stack:
    def fresh():
        s=stack.enter_context(socket.socket());s.settimeout(.25);return s
    def listener(backlog):
        s=fresh();s.bind(('127.0.0.1',0));s.listen(backlog);return s
    somaxconn=int(Path('/proc/sys/net/core/somaxconn').read_text())
    abort=int(Path('/proc/sys/net/ipv4/tcp_abort_on_overflow').read_text());assert abort==0
    small=listener(1);address=small.getsockname();before=counters();clients=[];timed_out=False
    for _ in range(8):
        client=fresh()
        try:client.connect(address)
        except TimeoutError:timed_out=True;client.close();break
        clients.append(client)
    depth=queued(address[1]);after=counters()
    assert timed_out and depth==len(clients) and 1<=depth<8,(depth,len(clients))
    changes={name:after[name]-before[name] for name in ['ListenOverflows','ListenDrops']}
    assert changes['ListenOverflows']>0 and changes['ListenDrops']>0,changes
    accepted=[]
    for client in clients:
        server,_=small.accept();stack.enter_context(server);accepted.append(server);server.sendall(b'ready')
        reader=stack.enter_context(client.makefile('rb'));assert reader.read(5)==b'ready'
    assert queued(address[1])==0
    for _ in range(4):
        client=fresh();client.connect(address);server,_=small.accept();stack.enter_context(server)
        server.sendall(b'ready');reader=stack.enter_context(client.makefile('rb'));assert reader.read(5)==b'ready'
        accepted.append(server);clients.append(client)
    assert len(accepted)>1 and all(s.fileno()>=0 for s in accepted)
    larger=listener(16)
    for _ in range(6):client=fresh();client.connect(larger.getsockname())
    assert queued(larger.getsockname()[1])==6
    print(json.dumps({'kernel':platform.release(),'somaxconn':somaxconn,'tcp_abort_on_overflow':abort,
        'requested_small_backlog':1,'queued_before_accept':depth,'next_connect_timed_out':timed_out,
        'counter_changes':changes,'accepted_connections_still_open':len(accepted),'larger_backlog_queued':6},sort_keys=True))
'''
result=subprocess.run(['docker','run','--rm','-i','--network','none',image,'python','-'],
    input=program,capture_output=True,text=True,timeout=30)
assert result.returncode==0,result.stderr
record=json.loads(result.stdout);print(json.dumps(record,sort_keys=True))

The first listener requests backlog one and deliberately does not accept connections. In the recorded environment, two client connects complete and /proc/net/tcp reports two queued connections. The next connect exceeds the client’s 250-millisecond timeout. The script records the observed depth rather than asserting that every Linux version must admit exactly two under this setting.

During that controlled overflow, both ListenOverflows and ListenDrops increase by one. Because the experiment has its own network namespace and only these loopback listeners, the observation can be associated with its deliberately stalled listener. No global kernel setting is changed, and the example does not manufacture failure by altering the shared Docker VM’s networking policy.

The application then accepts the queued connections and sends a short acknowledgement over each returned socket. The clients read the complete acknowledgement, and the listener queue falls to zero. Four more connections are created and promptly accepted while the previous accepted sockets remain open. Six accepted connections coexist despite the original requested backlog of one.

A second listener requests backlog sixteen. Six clients connect before any acceptance, and its queue reports six pending connections. This demonstrates that a larger queue can absorb this particular arrival burst while the application is paused. It does not show higher processing throughput, prove those clients received application responses, or establish that increasing a backlog solves sustained overload.

Tune acceptance and waiting capacity together

The recorded somaxconn value is 4096, comfortably above both requested backlogs. That value is an observation of the test environment, not a recommendation to copy into another host. Inspect the application’s actual listen argument, its accept loop, and the kernel cap together. A high system limit cannot enlarge a small backlog the application deliberately requests.

Separate incomplete handshakes from established connections awaiting acceptance. SYN-queue controls and SYN-cookie behavior answer different questions from the accept queue shown here. Likewise, tcp_abort_on_overflow changes overflow handling; enabling resets is not an increase in service capacity. This experiment leaves it disabled and demonstrates the client-visible timeout under that existing policy.

Finally, investigate why acceptance falls behind. Event-loop stalls, descriptor exhaustion, dispatch bottlenecks, and deliberate pauses can affect it differently from slow work on already accepted sockets. A larger queue buys waiting room, which must fit client deadlines and memory budgets. If sustained arrivals exceed the rate the service can accept and handle, additional waiting only postpones the capacity failure.

Sources

  • Linux listen(2), established connections waiting for accept and the somaxconn cap, checked 2026-09-12.
  • Linux accept(2), removing a pending connection and returning a connected socket, checked 2026-09-12.
  • Linux IP sysctl documentation, somaxconn, SYN backlog, and tcp_abort_on_overflow, checked 2026-09-12.