Five things, and only one of them is a socket
The rest of this series uses four words constantly and defines none of them. "Socket" appears nearly three hundred times across Parts 8 and 9. This part is the one that says what they are.
When you write net.createServer().listen(3000), five distinct things come into existence, in four different places, and people call at least three of them "the socket".
const server = net.createServer(); server.listen(3000); // server a JS object in V8's heap // server._handle a C++ TCPWrap // (inside it) a libuv uv_tcp_t // (inside that) fd 17, an integer in your process // (behind that) a socket, in the kernel
They are not synonyms and they do not have the same lifetime. The JS object can exist with no socket behind it; the socket can outlive the process that created it; and the integer 17 means nothing outside your process.
If you only take one thing: the file descriptor is a NUMBER, not a thing. It is an index into a table your process owns. Everything confusing about fds, from dup() to passing sockets between processes, follows from that one fact.
Which of these is the socket?
Each layer holds a reference to the one below it and adds something the layer below does not have. Click through them and note where each one lives, because the boundaries are where the confusion is.
read(), and bytes you wrote that are waiting for the network. Both are kernel memory charged to the socket — §9 walks them.The two boundaries worth naming now: the fd is meaningless outside your process, and the socket is meaningless outside the kernel. Everything above the fd is Node's bookkeeping; everything below it is the operating system's.
A number, a description, and the thing itself
Almost every confusing thing about descriptors comes from collapsing three levels into one. There are three, and they are shared differently.
dup() behaves the way it does.int a = open("f", O_RDONLY);
int b = dup(a); // same open file description: SHARED offset
int c = open("f", O_RDONLY); // new description: INDEPENDENT offset
read(a, buf, 10); // offset now 10 for BOTH a and b
// c is still at 0A descriptor is a reference, and the object behind it is reference-counted. close() does not close anything — it drops one reference. The socket goes away when the last reference does, wherever that reference happens to live.
EMFILE, and why servers hit it
The descriptor table is finite and the limit is per process. Every connection your server accepts consumes one entry, and so does every file, every pipe and every watcher.
$ ulimit -n 1024 # a common default — 1024 CONNECTIONS, not 1024 requests $ ls /proc/self/fd # your process's table, as files 0 1 2 3 17 18 19
When the table is full, accept() fails with EMFILE — and the connection stays in the accept queue rather than being refused, so the client sees a hang rather than an error. A server that leaks descriptors degrades slowly and then stops accepting entirely, with no crash to point at.
An idle connection costs a descriptor and some kernel memory, not CPU. That is why an event-loop server holds tens of thousands of them happily — and why the descriptor limit, not the CPU, is usually what caps it.
Not an object, not owned, not a resource the OS allocates
A port is a number in a range: 0 to 65535, because the field in the TCP header is sixteen bits wide. That is the whole definition. It is not a device, not a file, and not something the kernel creates.
EADDRINUSE is a duplicate-key error, nothing more exotic.CAP_NET_BIND_SERVICE to bind one. Nothing about port 80 is technically different from port 8080.Which raises the question the next panel answers: if a port is just a number in a table, how can one of them serve ten thousand simultaneous connections without ambiguity?
Not the port — the pair of pairs
A connection is identified by four values together: source address, source port, destination address, destination port. The kernel demultiplexes incoming segments by looking up that tuple, and any two connections that differ in any one of the four are distinct.
That is why your server needs exactly one listening socket on port 3000 to serve ten thousand clients. Every one of those connections has destination port 3000, and they are told apart entirely by the client's address and port.
A listening socket is identified by a PAIR — local address and local port. A connected socket is identified by all FOUR. That difference is the reason the two are different kinds of object, which is §9.
Where the other port comes from, and how you run out
The client never chooses its port. The kernel picks one from the ephemeral range when you connect(), and on Linux that range is about twenty-eight thousand numbers wide.
$ cat /proc/sys/net/ipv4/ip_local_port_range 32768 60999 # 28,232 ports # so ONE client can hold ~28k simultaneous connections # to ONE (destination ip, destination port) — and no more
The limit is per destination, not global, because the tuple includes the destination. Twenty-eight thousand connections to one upstream service exhausts it; the same client can open twenty-eight thousand more to a different one.
And this is where Part 11's TIME_WAIT comes back. A closed connection holds its tuple for roughly sixty seconds, so a client that opens and closes connections fast enough can exhaust the range with connections that are already finished. That is the shape of most "cannot assign requested address" incidents.
A listening socket is not a connection
"Socket" names two different objects with different fields, different lifetimes and different state machines. Almost every muddle in this area is these two being treated as the same thing.
When someone says "the socket closed", ask which one. A connected socket closing ends one conversation; the listening socket closing means your server stops accepting, and that is a completely different outage.
Buffers, queues, and who is allowed to touch them
Behind the descriptor is a kernel structure with real memory attached, and knowing which memory belongs to which object explains several things the rest of the series takes for granted.
read(). When it is empty, a non-blocking read gives you EAGAIN — Part 9's whole §6.write(), drained by the kernel as the peer's window and congestion control allow. When it is full, write() returns short — which is where Part 9's backpressure comes from.accept(). Sized by the backlog argument. Part 8 is largely about these two.None of this memory is in V8's heap, which is why a Node process holding ten thousand idle connections shows modest RSS and large kernel memory. The buffers are charged to the socket, not to your JavaScript.
Step the lines and watch the five layers appear
Now the two halves meet. Here is what each line of the opening example actually does, in terms of the five layers from §1 — which object comes into existence, which syscall fires, and what the kernel knows afterwards.
The shape to notice is how much of this is bookkeeping. createServer touches nothing outside V8. listen is where three syscalls fire in a row and every layer below the JS object comes into existence at once.
accept() gives you a descriptor; Node gives you a Socket
A connection arriving does not create a JS object by itself. Something has to call accept(), take the descriptor it returns, and build the layers back up — and that is the same five-layer chain in reverse.
connection handler is called with the top of it.Every connected socket in your process has its own descriptor, its own buffers and its own entry in the kernel's tuple table. Ten thousand connections is ten thousand of each — which is why §4's descriptor limit is usually the ceiling.
Ten claims about the primitives
These are the beliefs that make the rest of the series harder than it needs to be.
Nearly every wrong take here treats a NAME as a THING. A descriptor is a name for a description; a port is a name in a lookup table; "socket" is one name for two different objects.
If you remember nothing else
Everything the rest of this series says about I/O sits on top of these.
EADDRINUSE is a duplicate.accept() turns one into the other without consuming it.listen(), in three syscalls.Part 1 picks up from here with module resolution. Part 5 then follows a single fs.readFile down through these same layers to the disk and back.