HTTP is a protocol that runs on top of TCP. TCP is the actual transport layer — a stream of bytes between two machines, with delivery guarantees and ordering, but no concept of “requests” or “responses.” Node’s net module gives us direct access. We rarely need it, but when we do, nothing else will work.
In simple language: net is what http uses underneath. If we strip HTTP away, we’re just reading and writing bytes on a socket. That’s a TCP connection.
A minimal TCP server
import net from 'node:net';
const server = net.createServer((socket) => {
console.log('Client connected:', socket.remoteAddress);
socket.write('Welcome to the echo server\n');
socket.on('data', (chunk) => {
socket.write(`echo: ${chunk}`);
});
socket.on('end', () => {
console.log('Client disconnected');
});
});
server.listen(4000, () => console.log('TCP server on :4000'));
Test it from another terminal:
nc localhost 4000
# Welcome to the echo server
> hello
# echo: hello
That’s it. No paths, no methods, no headers. Just bytes in, bytes out.
A TCP client
import net from 'node:net';
const client = net.createConnection({ host: 'localhost', port: 4000 }, () => {
console.log('Connected');
client.write('ping\n');
});
client.on('data', (chunk) => {
console.log('Got:', chunk.toString());
client.end();
});
The socket is a duplex stream
A socket in Node is both readable ('data' events, for await ... of) and writable (write, end). It’s a duplex stream. Everything we know about streams applies.
The framing problem — why HTTP exists
Here’s the catch with raw TCP: there are no message boundaries. If a client calls socket.write('hello') then socket.write('world'), the server might see 'helloworld', 'hel' then 'loworld', or 'helloworld' all at once. TCP coalesces and splits at will.
In simple language: TCP is a pipe, not a stack of envelopes. We need to invent our own framing — like ending every message with \n, or prefixing each message with its length.
// length-prefixed framing
function send(socket, payload) {
const buf = Buffer.from(payload);
const len = Buffer.alloc(4);
len.writeUInt32BE(buf.length, 0);
socket.write(len);
socket.write(buf);
}
This is exactly the problem HTTP, MQTT, Redis’s RESP, and PostgreSQL’s wire protocol all solve in their own way. Frameworks like HTTP give us message boundaries for free.
When to use net vs http
Use net only when:
- You’re implementing a non-HTTP protocol. Custom binary protocols, game servers, IoT devices that speak Modbus / proprietary protocols, Postgres/Redis-style protocols.
- You’re building a proxy or load balancer and need to forward raw bytes.
- You need lowest possible overhead. No HTTP parsing, no headers. Real-time financial systems, telemetry pipelines.
- You’re tunneling something through SSH or a VPN socket.
Use http (or HTTP frameworks) when:
- You’re building anything that looks like a web service.
- You want to reuse browser tooling (curl, Postman, fetch).
- You want middleware, routing, JSON parsing — basically free.
For 99% of backend work, http is the right answer. net is for the 1% that’s genuinely lower-level.
Unix domain sockets
net can also do IPC over a filesystem path, no TCP involved. Way faster than localhost TCP when two processes on the same machine talk:
const server = net.createServer(handler).listen('/tmp/myapp.sock');
const client = net.createConnection({ path: '/tmp/myapp.sock' });
PostgreSQL, Docker daemon, and many cloud sidecars use this pattern.
Backpressure — same rules as streams
socket.write returns false when the kernel’s send buffer is full. If we ignore that and keep writing, memory balloons. Either await a 'drain' event, or use pipeline to glue streams together — it handles backpressure for us.
import { pipeline } from 'node:stream/promises';
await pipeline(source, socket); // backpressure-safe
The mental model
net gives us a byte pipe between two endpoints. No requests, no responses, no framing. We invent the protocol on top. It’s almost always the wrong choice for web work, and the only sensible choice for custom binary protocols. Knowing it exists — and that http is just bytes-with-rules layered on top — makes the whole networking stack much less mysterious.