TCP and Node.js Server Internals: A Deep Technical Dive
Links that I found interesting this week:
TypeScript 5.9 Beta - Microsoft announces TypeScript 5.9 beta with new language features, improvements to type checking, and enhanced developer experience.
Node.js in 2025 - Kashwin shows the future of Node.js, covering upcoming features, and performance improvements.
I/O Devices and Latency - PlanetScale demonstrates the relationship between I/O devices and latency, showcasing how hardware choices impact database and application performance.
TL;DR
Node.js networking isn't just about JavaScript, it's a chain of abstractions that starts at JavaScript land and goes deep to kernel-level TCP operations through multiple layers. Learn how
net.Socketandnet.Serverwork under the hood, see what happens when you callconnect()(from DNS resolution to TCP handshake), and understand how file descriptors and kernel sockets are used in most of Node.js servers (despite your library of choice, they are all using the same thing).
Network layers stack overview
To understand Node.js networking, you need to understand how it interfaces with the operating system's TCP/IP implementation. The TCP/IP model defines four protocol layers:
The TCP/IP protocol stack:
Application Layer: Protocols like HTTP, HTTPS, FTP, etc.
Transport Layer: TCP and UDP protocols
Internet Layer: IP protocol for routing and addressing
Network Access Layer: Ethernet, WiFi, and other link-layer protocols
Node.js provides JavaScript APIs that allow you to work with these protocols:
The
http/httpsmodules implement the HTTP protocol (Application layer)The
netmodule provides access to TCP functionality (Transport layer)The
dgrammodule provides access to UDP functionality (Transport layer)
Node.js doesn't implement these protocols directly—it uses the operating system's network stack through libuv.
The important component linking JavaScript and system networking is libuv's event loop. The poll phase of the event loop is particularly important for networking, as it receives new I/O events using platform-specific mechanisms: epoll on Linux, kqueue on macOS, and IOCP on Windows.
What happens when you create a Node.js server?
When you call net.createServer() or http.createServer or whatever alternative you have in your library of choice, a complex initialization sequence begins. The function creates a new net.Server that extends EventEmitter, initializes internal state variables, and prepares the connection handling pipeline.
An important part of the server creation process is to create an appropriate handle based on the type of connection. For TCP connections, Node.js creates a TCP handle through libuv. This happens when you call server.listen() with a port passed in:
Popular Node.js libraries like Fastify and Express use net.Server under the hood to start a new server. When you call listen on the server object from those libraries, at the end net.createServer gets called and creates a new server.
You can find more info about it if you look at the Fastify source code or Express source code.
net.Socket as a TCP socket wrapper and how it works
The net.Socket class is Node.js's representation of a TCP connection. It's important to understand that this JavaScript object is just a wrapper around system resources. When you create or receive a socket in Node.js, you're actually working with multiple layers of abstraction that connect your JavaScript code to kernel-level network operations.
For now, we're interested in the JavaScript Layer. The net.Socket object provides a high-level interface that extends both EventEmitter for event handling and Stream for data flow. Below this JavaScript layer, we have the C++ TCPWrap object, which binds to libuv's TCP handles, which in turn make system calls to create and manage kernel sockets.
Every net.Socket instance wraps what Node.js internally calls a "handle". It is a C++ object that represents the underlying system resource. This handle is stored in the socket's _handle property and provides the actual networking functionality:
Here is what happens when you call connect() on a socket:
Validation: Parameters are validated and normalized
Handle Creation: A TCP handle is created via libuv's
uv_tcp_init()DNS Resolution: Hostnames are resolved asynchronously through libuv's thread pool
Connection Initiation:
uv_tcp_connect()initiates the TCP three-way handshakeEvent Loop Integration: The file descriptor is registered with the platform's polling mechanism
Kernel sockets and file descriptors
Now let's get to the kernel level to understand what happens when connect() is called. At this layer, JavaScript abstractions give a way to operating system primitives.
When libuv calls the socket() system call, the kernel creates a struct socket and assigns it a file descriptor, a simple integer that serves as a handle to the kernel resource.
In Unix-like systems, the philosophy of "everything is a file" means TCP sockets are accessed through file descriptors, just like regular files. This abstraction allows the same system calls (read, write, close) to work across different resource types.
When your JavaScript socket.connect() goes through the layers, here's what happens in the kernel:
Socket Creation: The kernel allocates a struct socket containing:
Protocol operations (TCP in this case)
Socket state (connecting, established, etc.)
Send and receive buffers
Connection information (addresses, ports)
TCP Handshake: The kernel's TCP stack initiates the three-way handshake:
Sends SYN packet
Waits for SYN-ACK
Sends ACK
Transitions to ESTABLISHED state
Event Notification: The kernel notifies libuv through the platform's polling mechanism (
epoll/kqueue/IOCP) when the connection completes.
The kernel handles all TCP complexity—retransmissions, congestion control, packet ordering while Node.js event loop waits for events.









