One chain · four boundaries
Which of these is the socket?
Each layer holds a reference to the one below it and adds exactly one thing the layer below cannot express. And there are two of these chains, not one — accept() builds a second, and almost every confusion about sockets comes from assuming there is only ever one.
You call server.listen(3000).
Five things come into existence.
Four of them live inside your process, nested one inside the next.
One of them does not live in your process at all.
Only one of the five is the socket.
The four confusions this kills
"The file descriptor is the socket."
It is an index into a table your process owns. The socket is in the kernel and does not know your process exists. The clearest proof is that two descriptors can name one socket — which is not a curiosity, it is how cluster works. After a fork, the child inherits the parent's descriptor table, and both processes are now naming the same listening socket. Close one and the socket carries on serving.
"close() closes the socket."
close(17) removes one entry from your descriptor table and decrements the socket's reference count. Only when that count hits zero does the kernel begin tearing the connection down — and even then the socket does not vanish, it enters TIME_WAIT and holds its four-tuple for another minute or two. Every layer in your process can be gone while the socket is still very much alive.
"The server socket and the connection socket are the same socket."
They are two different kernel objects with two different jobs, and they have almost nothing in common. The listening one has queues and no buffers; the connected one has buffers and no queues. The listening one's address is half-empty — a port and an interface, no remote end. Not one byte of your traffic ever passes through the listening socket. Its only job is to manufacture the other kind, one accept() at a time.
Step four of the lifecycle above is the only point at which a second chain appears — the hinge the whole model turns on.
"Something's broken and I don't know where to look."
Because each layer adds exactly one thing, each layer can only fail in its own way. An error message usually names its layer, once you know the five.
Both chains at once
Two chains, nine objects, one line between what your process holds and what it merely names. Everything else on this page is a drill-down from this.
The kernel socket holds the bytes. The descriptor gives your process a way to name it. The libuv handle gets it registered with the loop, so something is told when it becomes ready. The bridge is the only layer that can enter JavaScript. And net.Server invents the vocabulary — "connection" is a string that exists nowhere below it.
Bytes, a name, readiness, a way into JavaScript, words for it.
The other half is the one most models leave out. accept() mints a whole second chain, and the two have independent lifetimes. That single fact answers why server.close() does not drop your connections, why the listening socket has no buffers, and why a connection survives the server that produced it.
Part two · File descriptors — three levels, not one
A number, a description, and the thing itself
Almost every confusing thing about descriptors comes from collapsing three levels into one. There are three, they are shared differently, and close() only ever touches the first of the two arrows between them.
The fourth layer of the chain in part one was drawn as a single box holding nothing. That was true at the resolution of that figure and false at this one. The integer is level one. Between it and the socket sits a second kernel object that nothing in part one named.
"Two descriptors on one file behave the same however they were made."
A dup and a second open produce table entries that look identical and behave differently in a way nothing in the number reveals. One shares an offset and a set of status flags; the other shares neither. The difference is which arrow was copied — and the arrow is not something the descriptor can be asked about.
Three doors to a second reference
A second descriptor never appears by accident. There are exactly three ways one comes to exist alongside another, and all three end at the same place: one more arrow into an open file description that already existed. Nothing is duplicated at level two or three — a count goes up.
In all three, one object gains a reference. Nothing below level one is ever copied.
Which level owns what
Because the levels are shared differently, the level a property lives on decides who else is affected by changing it. Close-on-exec and a non-blocking flag sound like the same kind of thing and are not: one is private to a single descriptor, the other reaches every process holding a reference.
"A closed descriptor is gone."
The object it named may well be gone. The number is back in circulation immediately, and the kernel always hands out the lowest free one — so the next open or accept anywhere in the process is likely to receive it. Code still holding the old number is now operating on something else entirely, and every call it makes succeeds.
This is why a stale descriptor is worse than a dangling pointer. A dangling pointer usually crashes. A stale descriptor writes to whatever took the slot.
The table has a size
The descriptor table is finite and the limit is per process. Every connection consumes an entry, and so does every file, every pipe, each child process's three stdio ends, and the epoll or kqueue instance the event loop itself runs on.
"Out of descriptors means the server refuses connections."
Nothing is refused. The kernel completes the handshake and queues the connection whether or not any process can pick it up, so the accept queue keeps filling while accept() keeps failing. On a naive accept loop the descriptor stays readable forever, the loop is woken continuously, and the process spins at full CPU while every client waits on a connection that will never be served.
Node does not do that. libuv opens a spare descriptor when the loop starts and holds it in reserve; on EMFILE it closes the spare, drains the pending connections by accepting and immediately closing each one, then reopens the spare. The result is a server that sheds load — clients see a connection that closes at once, which is a far better failure than silence.
What an idle connection actually costs
A connection sitting open with nothing happening on it costs a descriptor and a few kilobytes of kernel memory — a socket structure and its two buffers. It costs no CPU at all. The event loop is registered with the kernel for readiness, and a socket that is not ready never wakes anything.
That is the whole reason an event-loop server can hold tens of thousands of open connections without effort, and it is why the number that caps concurrency is almost never the processor. Ten thousand idle sockets is a few tens of megabytes of kernel memory, which is nothing; ten thousand descriptors against a soft limit of 1024 is fatal.
A thread-per-connection server pays for every idle connection twice: once for the same descriptor and socket buffers, and again for a thread — a stack reservation and an entity the scheduler must consider on every context switch. There, memory and scheduling overhead cap concurrency first. On an event loop those second costs are absent, so the ceiling moves to the descriptor table, and the descriptor table is a number that can be raised.
One resource is commonly mistaken for this one. A socket in TIME_WAIT holds no descriptor — the descriptor was released when it was closed, and the slot is free. What it holds is a four-tuple, and running out of those is a different exhaustion with a different limit and a different symptom. Descriptor pressure shows up as EMFILE; tuple pressure shows up as EADDRNOTAVAIL when connecting out, or EADDRINUSE when binding — two different errors on two different sides, taken up in part three.
Two arrows, three lifetimes
A descriptor is a reference, and there are two counts between it and the thing it names. The description counts the descriptors reaching it. The socket or file counts the descriptions reaching it. close() decrements the first, and only a first count reaching zero decrements the second.
That is the whole of it, and it explains the rest by itself: why a fork makes closing a socket a two-process job, why a seek through one descriptor moves another, why a listening socket outlives the process that made it once a worker holds a reference, and why the number that came back from the last open is a fact about a table rather than a fact about a file.
Part three · Ports — sixteen bits
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 on request.
Two of those fields sit at the very start of every segment, and one table in the kernel maps numbers to sockets. bind() inserts a row; an arriving segment's destination field is the key that gets looked up. Nothing is allocated and nothing is owned.
Binding is an insert. A port is never created or destroyed — rows are added to a table and removed from it.
"The OS allocates a port to my process."
Nothing is allocated. bind() adds a row to a lookup table and close() removes it. The number existed before the program started and will exist after it exits, because it is a range of values a header field can hold, not an object with a lifetime. A process does not own a port either — the row points at a socket, and part two showed how many processes can hold a reference to one of those.
How one port serves ten thousand connections
If a port were simply a key mapping to a socket, a second connection to the same port would have nowhere to go. It works because the kernel does not consult one table — it consults two, in order, and the first is keyed by far more than the port.
The listener table's key has only two fields, which raises a question the four-tuple table never faces: what happens when two binds overlap. The answer is that the most specific one wins — a socket bound to 127.0.0.1:3000 takes precedence over one bound to 0.0.0.0:3000 for traffic arriving on loopback, and the wildcard takes everything else.
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 separates arriving segments by looking up that tuple, and any two connections differing in any one of the four are distinct.
"One port, one connection."
The port is one field of four. A server on :3000 holds as many connections as there are distinct tuples reaching it, and since every client contributes its own address and source port, the ceiling is not on the server's side at all. Which is the useful inversion: the party that runs out of ports is the client, and it runs out separately for each destination it talks to.
Where the other port comes from
The client never chooses its port. The kernel picks one from the ephemeral range on connect(), and that range is a fraction of the space — not the 65,536 the sixteen bits suggest.
A closed connection holds its tuple in TIME_WAIT for roughly sixty seconds, and it is the side that closes first which holds it — usually the client, on outbound work. So a client opening and closing connections quickly exhausts its range with connections that have already finished. That is the shape of nearly every "cannot assign requested address" incident, and it is why the arithmetic understates the problem.
Three ways a port is shared or chosen
Two socket options and one special value cover almost every question about who may bind what. They are routinely confused with each other, and only one of them permits two live listeners.
One caveat on the second: when a socket leaves a SO_REUSEPORT group, handshakes already tied to it are dropped rather than redistributed — so a rolling restart under it can reset connections that were mid-handshake.
"EADDRINUSE means another program has port 3000."
It means the bind overlaps something already bound, which is not the same as matching it. 0.0.0.0:3000 conflicts with 127.0.0.1:3000 because the wildcard covers it; 127.0.0.1:3000 and 192.168.1.5:3000 coexist because they do not. And the commonest cause is not another program at all — it is a connection of your own still in TIME_WAIT, which is exactly what SO_REUSEADDR exists to permit, and why Node sets it by default.
Four errors, four different places
The failures around ports are routinely conflated, and each one names a distinct stage. Two are about binding, two are about connecting, and only one of them involves a process at the far end.
"Running out of ports is a server problem."
A server's capacity is bounded by descriptors, not ports — part two's ceiling. Ports run out on the client, per destination pair, because three of the four fields are pinned and only the source port varies. The symptom is EADDRNOTAVAIL on connect(), not EADDRINUSE on bind(), and the fix is connection reuse rather than a bigger machine.
Sixteen bits and a table
A port is a field in a header and a key in a lookup. The kernel consults the four-tuple table first and the two-field listener table only on a miss, which is why one listening number serves thousands of connections and why the identity of a connection is never the port alone.
And the number that runs out is the one nobody chooses. The client's kernel draws a source port from a band of roughly twenty-eight thousand, per destination pair, and holds each one for a minute after the connection ends.
Part four · Sockets — two kinds, one word
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 those two being treated as the same thing.
Part one drew them as two chains and part three showed why one local port can serve both. This is the field-by-field difference, and the memory behind each.
One has queues, the other has buffers, and they are never the same object.
When someone says "the socket closed", the useful question is which one. A connected socket closing ends one conversation. The listening socket closing means the server stops accepting, every existing conversation carries on, and that is a completely different outage — invisible to anyone already connected and total for everyone else.
Buffers, queues, and who is allowed to touch them
Behind the descriptor is a kernel structure with real memory attached. Which memory belongs to which object explains several things the rest of this takes for granted — including why the two objects can never be the same one.
A Node process holding ten thousand idle connections shows modest RSS and large kernel memory, because all of this is charged to the socket rather than to your JavaScript.
"Ten thousand connections at 128KB of buffer each is 1.3GB."
The buffer is sized at 128KB and allocated as it is used. Linux charges pages to a socket when bytes actually occupy them, so an idle connection costs the socket structure and almost nothing else. The size is a ceiling on one connection's window, not a reservation — which is why the honest per-connection cost is a descriptor and a couple of kilobytes, and why the limit that bites first is part two's table rather than memory.
The receive buffer is the window
The most useful fact about that memory is not its size. It is that the free space left in the receive buffer is the window advertised to the peer — so the buffer is not merely where bytes wait, it is the mechanism by which a slow reader slows down a fast sender.
"write() returning false means the network is full."
It means the JavaScript queue is full. There are two queues on the write path and only the lower one is kernel memory: bytes go to the socket send buffer until that is full, and only the overflow accumulates in the stream's own buffer in V8's heap. write() reports on the upper queue against its highWaterMark, so by the time it returns false the kernel buffer has already been full for some time. TCP's backpressure begins a whole buffer earlier than Node's does.
What closing actually does
A connection has two directions and they close independently. Which side sends the first FIN decides which side pays for it afterwards, and one of the two resting states means a bug rather than load.
"CLOSE_WAIT and TIME_WAIT are both just connections winding down."
They sit on opposite sides and mean opposite things. TIME_WAIT is on the side that closed first and clears itself after about a minute — normal, and only a problem in bulk because it consumes tuples. CLOSE_WAIT is on the side that has not closed, and it clears when your code calls close and never before. A page of TIME_WAIT is traffic. A page of CLOSE_WAIT is a handler that returned without ending its socket, holding a descriptor the whole time.
Four ways a connection ends
They differ in what the peer sees and in what is left behind, and two of them skip the orderly shutdown entirely.
Not every socket is a TCP connection
The word covers more than the two objects above. Each kind answers "what identifies it" differently, and the differences explain which techniques apply where.
"A UDP socket is a connection without the handshake."
It has no connection at all, and therefore no state machine, no four-tuple identity and no send buffer holding unacknowledged bytes. What it has is a queue of whole datagrams — so a read returns one message rather than a slice of a byte stream, and two messages never merge the way two TCP writes can. Nothing in part four's state diagram applies to it.
One word, two objects
A listening socket is identified by two fields, holds two queues, has one state and never carries a byte. A connected socket is identified by four fields, holds two buffers, walks a state machine and carries everything. accept() is the factory between them, and the only thing they share is a local port.
Behind both is kernel memory your process cannot reach except through read() and write(). The free space in the receive buffer is the window the peer is allowed to fill, which makes a slow reader into a slow sender without either side deciding to slow down. And when it ends, the side that sends the first FIN pays with a minute of TIME_WAIT, while the side that never sends one holds a descriptor open until somebody notices.
Part five · the chain, both ways
Which call creates which layer
Every part so far described objects. This one describes the calls that make them, and it runs in more than one direction: your code builds the chain downward, the kernel rebuilds it upward, and closing tears it down along a third path that is the reverse of neither.
The lanes below are the layers from part one. An entry in the right-hand lane is a transition into the kernel; everything else is bookkeeping inside your own process.
"createServer opens the port."
It creates one JavaScript object and fires nothing at all. No descriptor, no kernel object, no syscall — the port is untouched and another process could still bind it. Everything below the JavaScript layer comes into existence inside listen(), which is also where the failure arrives: a port conflict cannot surface at createServer time because nothing has been claimed yet.
One line at a time, five layers deep
Two lines of ordinary code, and the shape worth noticing is how much of it is bookkeeping. One of the five layers exists before anything crosses into the kernel, and there is a moment in the middle where a libuv handle exists with no descriptor at all.
"The five layers appear together."
They appear in a fixed order with two gaps that matter. uv_tcp_init builds a handle whose descriptor field is -1 — libuv defers creating the socket until the first bind — so for a moment a uv_tcp_t exists and a descriptor does not. And at the other end, an accepted socket is not registered with the event loop until something reads from it, so a connection you accept and ignore is invisible to epoll entirely.
accept() gives you a descriptor; Node gives you a Socket
A connection arriving does not create a JavaScript object by itself. Something has to call accept(), take the descriptor it returns, and build the layers back up — the same five-layer chain, in reverse, for one connection only.
The listening socket makes sockets and never becomes one. Its descriptor is unchanged throughout and goes straight back to waiting.
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 part two's descriptor limit is usually the ceiling, and why the one syscall per connection is never the thing that hurts.
"accept() creates the connection."
The kernel completed the three-way handshake on its own, before any code of yours ran, and parked the finished connection in a queue. accept4 pops one off and returns a descriptor for an object that already exists — which is why a connection can sit fully established in the accept queue of a process that is too busy to take it, and why part two's EMFILE leaves clients waiting on connections that are already open.
Four directions, and one of them is standing still
The same chain is traversed by different things for different reasons, and the costs are not symmetric.
What the kernel actually sees
Stripped of the JavaScript and C++ lanes, the whole of the above is seven lines. Their absence is as informative as their presence: everything not listed here happened inside the process, at no cost to anything outside it.
The chain, both ways
Downward, your code causes the layers: five syscalls, once, and afterwards a passive socket with two queues and a loop asleep in epoll_wait. Upward, the kernel causes them: one syscall per connection, four objects rebuilt on top of the descriptor it hands back, and the listening socket untouched throughout. Outward, one syscall releases the descriptor immediately and leaves a JavaScript object the collector still has to take.
The asymmetry is the useful part. Building the chain is something you do and it returns; rebuilding it is something done to you and it arrives as a callback. Which is why the first happens once per server and the second happens ten thousand times, and why the cost that matters is never the syscall.
Part six · the models worth correcting
Ten claims about the primitives
These are the beliefs that make everything above harder than it needs to be. Ten statements, three of them true.
The corrections are printed either way. A guess committed to is worth more than a correction read cold.
Nearly every one of them treats a name as a thing. A descriptor is a name for a description, which is itself a name for a socket. A port is a name in a lookup table. "Socket" is one name for two different objects. Once a name is mistaken for the thing it names, sharing looks like copying, closing looks like destroying, and a number looks like a resource.
If you remember nothing else
Everything above sits on top of six sentences, one from each part. Each opens to the version with the mechanism in it.
Six sentences, one chain. Every figure above is a drill-down from one of them.
One chain, six ways of looking at it
Part one drew the chain and named the layers. Part two took the fourth layer apart and found three levels inside it. Part three took the fifth and found sixteen bits and a table. Part four found that the fifth layer is two different objects sharing a word. Part five walked the calls that build the whole thing, in three directions.
The through-line has not changed since the first figure: each layer adds exactly one thing the layer below it cannot express, and it learned how to reach that layer at registration time. Bytes, a name, readiness, a way into JavaScript, words for it — and underneath all of it, one object in kernel memory that none of the other four ever becomes.