Skip to main content

Command Palette

Search for a command to run...

Understanding How the Kernel Handles TCP Connections Internally

Published
β€’8 min read
Understanding How the Kernel Handles TCP Connections Internally

When building high-scale backend systems or WebSocket/real-time apps, we often think only in terms of frameworks and application logic. But behind every HTTP/WebSocket request is a carefully orchestrated system designed by the Linux kernel.

This post dives deep into:

  1. How the kernel handles the TCP 3-way handshake using queues

  2. How a Node.js process receives connections and file descriptors

  3. Default limits on SYN queue, Accept queue, FD table, Send/Receive buffers

  4. How server response time affects scalability by freeing resources faster

Let’s make these internal mechanics crystal clear.


🧩 1. How the Kernel Handles TCP 3-Way Handshake Using Queues

When a client connects to your server, the connection does not go directly to your Node.js app.
It passes through two kernel-managed queues during the TCP handshake:

πŸ”Ή Step 1: Client sends SYN β†’ goes to the SYN Queue

TCP handshake begins:

Client β†’ SYN β†’ Server

The server kernel:

  • Stores this half-open connection in the SYN queue

  • Sends back SYN-ACK

Now the kernel waits for the final ACK.


πŸ”Ή Step 2: Client sends ACK β†’ moves to the Accept Queue

When the final ACK arrives:

Client β†’ ACK β†’ Server

The connection is now fully established, but Node.js STILL doesn’t get it.

The kernel moves this connection into the Accept Queue, which stores connections waiting for the process to call accept().


πŸ”Ή Diagram: SYN Queue β†’ Accept Queue β†’ Node.js

Incoming Connections
        |
        v
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   Half-open (SYN)
 |     SYN Queue      |
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        |
Handshake complete
        v
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   Fully established
 |   Accept Queue     |
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        |
   accept()
        v
 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 | Node.js Process    |
 | FD Table Entry     |
 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Only after the process calls accept() does the connection enter the Node.js runtime.


🧩 2. How a Node.js Process Gets the Connection & FD

After the connection sits in the Accept Queue, Node.js eventually calls:

server.on("connection", socket => {
  // This socket is backed by a kernel FD
});

Internally:

  1. Node.js (via libuv) calls the accept() system call

  2. The kernel:

    • Removes a connection from the Accept Queue

    • Creates a File Descriptor (FD) for the process

    • Returns an integer like 4, 5, 6… representing the socket

βœ” What is an FD?

A File Descriptor is an index inside the process FD table, pointing to the kernel’s socket object.

Example:

FD 3 β†’ listening socket
FD 4 β†’ client #1
FD 5 β†’ client #2
FD 6 β†’ client #3

Node.js never sees the TCP internals.
It only works with FDs and high-level net.Socket objects.


🧩 3. Default Limits: SYN Queue, Accept Queue, FD Limit, Buffers

Here are the default kernel limits that affect how many connections your server can handle.


πŸ”Ή A. SYN Queue Limit

Controls how many half-open connections the kernel will track:

/proc/sys/net/ipv4/tcp_max_syn_backlog

Typical default: 256 – 4096

If full β†’ kernel may drop SYNs or activate SYN cookies.


πŸ”Ή B. Accept Queue Limit

Controls how many fully established connections are waiting for accept():

Effective size =

min(backlog passed to listen(), net.core.somaxconn)

Typical defaults:

  • backlog (Node.js) = 511

  • somaxconn = 128

So effective Accept Queue = 128 entries

If full β†’ kernel drops new connections.


πŸ”Ή C. File Descriptor Limit (per process)

Default on Ubuntu: 1024

Meaning:

πŸ‘‰ A Node.js process can hold only ~1000 simultaneous TCP connections unless increased.


πŸ”Ή D. Socket Send & Receive Buffers

Each TCP connection uses kernel buffers:

sndbuf (send buffer)

Stores outgoing data waiting to be transmitted.

rcvbuf (receive buffer)

Stores incoming data before Node.js reads it.

Defaults:

  • 16 KB – 64 KB per buffer

  • Auto-tuned dynamically by the kernel based on congestion

Each connection roughly consumes:

~32 KB to 128 KB in kernel memory

Multiply this by 10k sockets = hundreds of megabytes.


Summary Table of Defaults

ComponentDefault Value
SYN Queue per socket/port ( 3000 )256–4096
Accept Queue per socket/port ( 3000 )128 (somaxconn)
Node backlog ( can used to override the above queue limits )511 (but capped by 128)
FD per process1024
sndbuf~16–64 KB
rcvbuf~16–128 KB

🧩 4. How Response Time Affects Server Scalability (Indirectly)

βœ” Faster response time = FDs freed sooner

βœ” Fewer active connections = Accept Queue stays empty

βœ” Kernel buffers clear faster

βœ” Server can accept more new connections

βœ” Lower backlog pressure = fewer dropped connections

Let’s break it down.


A. The FD Is Occupied for the Entire Duration of an HTTP Request

Every incoming HTTP request creates or uses a TCP connection:

When a request arrives:

  • Node.js accepts the connection

  • Kernel gives Node a file descriptor (FD)

  • Node uses that FD to read the request and send the response

While the request is being processed:

The FD is locked by the server until:

  1. Server finishes sending the response

  2. The connection is closed OR reused

  3. Kernel frees the FD entry

  4. FD is returned to the pool of available FDs

This means:

The longer a request takes, the longer the FD stays occupied.


B. When Response Time Is Slow, FDs Start to Accumulate

Let’s assume:

  • FD limit per process = 1024

  • Average response time = 500ms

This means:

Every request holds 1 FD for 0.5 seconds

So the FD table is constantly filled like this:

TimeActive requestsFree FD slots
0s300free = 724
0.1s700free = 324
0.2s900free = 124
0.3s1024free = 0 (FD table full!)

Once the FD table hits 1024, the server cannot accept a single new connection.

At this point:

βœ” Accept queue fills

βœ” SYN queue eventually fills

βœ” Kernel drops new connections

βœ” Clients see:

  • Timeouts

  • Connection refused

  • Random failures

The server appears down even if the CPU is only 5% used.


C. Faster Response Time Frees FDs Faster

Now imagine response time drops from 500ms β†’ 50ms.

That is a 10x improvement.

Meaning each FD is held for 10x less time.

Now the FD table is freed 10x faster.


D. Mathematical Explanation of FD Reuse Rate

Formula:

Requests per second (max) = FD_limit / average_response_time

Where response_time is in seconds.


πŸ“Œ Case 1 β€” Response Time = 500ms (0.5 sec)

1024 / 0.5 = 2048 requests/sec max

πŸ“Œ Case 2 β€” Response Time = 50ms (0.05 sec)

1024 / 0.05 = 20480 requests/sec max

βœ” SAME hardware

βœ” SAME FD limit

βœ” SAME kernel buffers

βœ” SAME code

But a 10x faster response time β†’ 10x more throughput
Because each FD is reused 10x faster.

This is one of the most mind-blowing insights of server design.


E. What Happens When FD Limit Is Hit? (Deep Kernel-Level Breakdown)

Let’s say all 1024 FDs are in use.

Now a new client tries to connect.

Step-by-step failure sequence:


❌ Step 1 β€” Node calls accept(), kernel returns EMFILE

Node.js tries to accept the new connection:

accept() β†’ error: EMFILE (Too many open files)

Meaning:

The process cannot accept ANY more connections until an FD frees up.

Now the ACCEPT QUEUE starts filling because Node is no longer accepting connections.


❌ Step 2 β€” Accept Queue (max 128) fills up

Kernel accepts new handshakes and stores them in the accept queue.
Once 128 connections have accumulated:

accept queue full β†’ drop new established connections

Clients see:

ECONNRESET
Connection refused
Timeout

❌ Step 3 β€” SYN Queue eventually fills

If the server is still overloaded:

tcp_max_syn_backlog reached

Kernel stops storing half-open connections.

Now new clients cannot even handshake.


πŸ”₯ At this point, even though:

  • Your server has low CPU usage

  • Application is not "busy"

  • Nothing is crashing

The TCP connection pipeline is jammed due to FD exhaustion.

This is EXACTLY why response time matters.


F. Visual Timeline Comparison

Slow server (RT = 500ms)

Requests pile up β†’ FD table fills β†’ accept queue fills β†’ SYN queue fills β†’ server stops accepting

Fast server (RT = 50ms)

FDs free quickly β†’ accept queue remains empty β†’ SYN queue stays clean β†’ server sustains huge spikes

G. The Non-Obvious Truth

Improving response time increases server capacity even if CPU is not the bottleneck.

This is because your bottleneck becomes:

  • FD usage

  • kernel buffers

  • accept queue behavior

  • TCP backpressure

NOT CPU cycles.


⭐ Final Summary

βœ” FDs stay occupied for the entire duration of the request

βœ” Slower responses β†’ FDs remain busy longer

βœ” Fast responses β†’ FDs get freed faster

And freeing FDs faster:

  • Prevents accept queue from filling

  • Prevents SYN queue overload

  • Reduces dropped connections

  • Increases maximum RPS

  • Enables kernel to handle spikes

  • Improves stability under load


✨ Conclusion

Response time is not only a UX metric.
It is also a scalability metric.

A fast server:

  • Uses fewer FDs

  • Frees kernel buffers quicker

  • Keeps Accept Queue empty

  • Handles connection spikes better

  • Can accept more users at the same time

Good backend performance is not just about speed β€”
it’s about resource turnover.


🏁 Final Thoughts

Understanding kernel-level TCP behavior helps us design better, more scalable systems.
Once you understand:

  • How SYN queue and Accept queue work

  • How FDs and buffers are allocated

  • How response time impacts server capacity

…you start to see backend performance in a whole new light.

If you're building Node.js APIs, WebSocket servers, or real-time apps, this knowledge directly translates into:

  • Better architecture

  • Better scaling strategies

  • Better production readiness

Disclaimer: This understanding of how response time influences scalability is a theoretical conclusion drawn from how the kernel handles TCP connections, queues, and file descriptors. I may still be learning, so if you have different perspectives or deeper insights, I’d love to hear them β€” feel free to share your thoughts or challenge this view!