<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Sysctl · XB Field Notes</title><link>https://www.xbcatm.com/tags/sysctl/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://www.xbcatm.com/tags/sysctl/index.xml" rel="self" type="application/rss+xml"/><item><title>The TCP backlog holds connections that are still syncing</title><link>https://www.xbcatm.com/posts/sysctl-tcp-max-sync-backlog/</link><pubDate>Fri, 24 Mar 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/posts/sysctl-tcp-max-sync-backlog/</guid><description>The listen backlog caps how many half open connections the kernel queues for a socket until the app accepts them.</description><content:encoded>&lt;p&gt;A listening TCP socket has two queues, not one, and the sysctl this article is named after governs the queue that almost never drops anything. The backlog passed to &lt;code&gt;listen(2)&lt;/code&gt; is the accept queue, holding connections that finished the handshake and await the application; the SYN queue holds half-open requests. Confusing them produces the wrong fix.&lt;/p&gt;
&lt;p&gt;That matters because the famous advice, raise &lt;code&gt;tcp_max_syn_backlog&lt;/code&gt;, is largely obsolete: &lt;code&gt;tcp_syncookies&lt;/code&gt; defaults to 1, and as listen(2) puts it, with syncookies enabled there is no logical maximum length and that setting is ignored. What actually bites is the accept queue and how fast the application calls &lt;code&gt;accept()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="the-backlog-argument-is-the-accept-queue"&gt;The backlog argument is the accept queue&lt;/h2&gt;
&lt;p&gt;listen(2) is explicit that the behaviour changed with Linux 2.2: &lt;code&gt;backlog&lt;/code&gt; now specifies the queue length for completely established sockets waiting to be accepted, not the number of incomplete requests. The incomplete queue is bounded separately by &lt;code&gt;/proc/sys/net/ipv4/tcp_max_syn_backlog&lt;/code&gt;. And if the backlog argument exceeds &lt;code&gt;/proc/sys/net/core/somaxconn&lt;/code&gt; it is silently capped to that value, 4096 since Linux 5.4 and 128 before. So &lt;code&gt;listen(8192)&lt;/code&gt; on a default host gives you 4096, and nothing logs it.&lt;/p&gt;
&lt;p&gt;tcp(7) describes &lt;code&gt;tcp_max_syn_backlog&lt;/code&gt; as the maximum number of queued requests that have not received an acknowledgement, defaulting to 256, raised to 1024 when memory is at least 128 MB, cut to 128 below 32 MB. ip-sysctl adds the qualifications: it is a per-listener limit, each half-open request costs about 304 bytes, and &lt;code&gt;tcp_syncookies&lt;/code&gt; defaults to 1, sending a cookie on overflow instead of dropping.&lt;/p&gt;
&lt;h2 id="with-syncookies-on-the-syn-queue-stops-mattering"&gt;With syncookies on, the SYN queue stops mattering&lt;/h2&gt;
&lt;p&gt;ip-sysctl documents &lt;code&gt;tcp_syncookies&lt;/code&gt; as a fallback that must not be used to help a loaded server stand against legal connection throughput, and says syncookies seriously violate the protocol, disallow TCP extensions, and can degrade services. listen(2) goes further: with syncookies enabled there is no logical maximum length and &lt;code&gt;tcp_max_syn_backlog&lt;/code&gt; is ignored. tcp(7) still describes the same knob as dropping requests once exceeded, which is the syncookies-off behaviour. Both pages are right about different configurations.&lt;/p&gt;
&lt;p&gt;The honest reading is therefore: leave syncookies at 1 and stop tuning the SYN queue. The cost is that the fallback loses TCP options such as window scaling and SACK on the connections it rescues, which is a throughput problem rather than a connectivity one. If you see SYN-flood warnings while the traffic is legitimate, ip-sysctl&amp;rsquo;s own advice is to tune the other knobs, not to disable syncookies.&lt;/p&gt;
&lt;h2 id="the-accept-queue-is-where-the-pressure-lands"&gt;The accept queue is where the pressure lands&lt;/h2&gt;
&lt;p&gt;With the SYN queue neutralised, the queue that fills is the accept queue, sized by &lt;code&gt;min(listen(backlog), somaxconn)&lt;/code&gt;. Overflow behaviour depends on &lt;code&gt;tcp_abort_on_overflow&lt;/code&gt;, default FALSE: ip-sysctl says the default lets a connection recover if the overflow was a burst, and warns that enabling the abort option can harm clients. The mechanism is the surprise: by default the kernel silently discards the final ACK, so &lt;code&gt;connect()&lt;/code&gt; succeeds and the failure only surfaces when the first request stalls or is reset.&lt;/p&gt;
&lt;p&gt;A bigger backlog is a delay line, not capacity. In the model, &lt;code&gt;listen(128)&lt;/code&gt; and &lt;code&gt;listen(65535)&lt;/code&gt; accept the same 20,000 connections in a second; the larger backlog only postpones the overflow, because the accept rate is set by the worker count.&lt;/p&gt;
&lt;h2 id="when-it-breaks"&gt;When it breaks&lt;/h2&gt;
&lt;p&gt;The first failure is the silent one, and it is the default. A service whose accept loop is too slow overflows the accept queue, the kernel drops the final ACKs, and clients see connections that establish and then hang. Nothing is refused, so a client connection pool fills with dead sockets and the error surfaces as timeouts somewhere unrelated. Watch &lt;code&gt;TcpExtListenOverflows&lt;/code&gt; and &lt;code&gt;TcpExtListenDrops&lt;/code&gt;: those counters climbing while the accept rate is flat means the handler is the limit.&lt;/p&gt;
&lt;p&gt;The second failure is the dropped SYN, which needs syncookies off to happen. A full SYN queue then drops the request and the client retransmits on a schedule whose documented endpoints are 67 seconds to the last retransmission and 131 seconds to the final timeout, so the user-visible symptom is a connect that hangs for over two minutes and then fails. Raising &lt;code&gt;tcp_max_syn_backlog&lt;/code&gt; is a real fix in that configuration, and turning syncookies back on is a better one, if you accept the loss of TCP extensions.&lt;/p&gt;
&lt;h2 id="check-it-quickly"&gt;Check it quickly&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ss -lnt &amp;#39;sport = :8443&amp;#39;
nstat -az TcpExtListenOverflows TcpExtListenDrops TcpExtTCPReqQFullDrop TcpExtTCPReqQFullDoCookies
sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.ipv4.tcp_syncookies net.ipv4.tcp_abort_on_overflow
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In &lt;code&gt;ss -lnt&lt;/code&gt;, &lt;code&gt;Send-Q&lt;/code&gt; is the backlog the socket was created with and &lt;code&gt;Recv-Q&lt;/code&gt; is how many established connections are queued and unaccepted, so &lt;code&gt;Recv-Q&lt;/code&gt; sitting at &lt;code&gt;Send-Q&lt;/code&gt; is the queue being full. The &lt;code&gt;nstat&lt;/code&gt; counters should be zero or near it: &lt;code&gt;TcpExtListenOverflows&lt;/code&gt; and &lt;code&gt;TcpExtListenDrops&lt;/code&gt; are accept-queue events, while &lt;code&gt;TcpExtTCPReqQFullDrop&lt;/code&gt; is a dropped SYN and &lt;code&gt;TcpExtTCPReqQFullDoCookies&lt;/code&gt; is the fallback doing its job. The &lt;code&gt;sysctl&lt;/code&gt; line shows what the kernel will honour, and if &lt;code&gt;somaxconn&lt;/code&gt; is smaller than your &lt;code&gt;listen()&lt;/code&gt; argument then that smaller number is your real backlog.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.kernel.org/doc/html/latest/networking/ip-sysctl.html"&gt;Kernel ip-sysctl documentation&lt;/a&gt;, somaxconn, tcp_max_syn_backlog, tcp_abort_on_overflow and the syncookies warning, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/listen.2.html"&gt;Linux listen(2)&lt;/a&gt;, the accept queue since Linux 2.2, the silent somaxconn cap, and syncookies removing the SYN-queue bound, checked 2026-09-16.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/tcp.7.html"&gt;Linux tcp(7)&lt;/a&gt;, tcp_max_syn_backlog defaults by memory size and the syncookies caveats, checked 2026-09-16.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>