Deep Engineering
Intermediate·Published·25 MIN

TIME_WAIT and ephemeral ports: the client hits the wall first

Tens of thousands of sockets in TIME_WAIT look like a leak, but the state is normal — and it usually gets treated with the wrong tool. Measured: on an ordinary close the sockets go to the side that closes actively; the state lasts a minute; and what the client runs out of is not memory but port numbers. The settings people turn are documented in tcp(7) — and none of them is about the duration.

Full technical treatment

TL;DR

A closed connection does not vanish at once: the side that closed it holds on to its address-and-port pair for a while. That is TIME_WAIT, and it exists so that segments of the old connection delayed somewhere in the network are not accepted by a new connection with the same addresses and ports. It is the normal end of a conversation, not a leak and not a fault.

Hence the main consequence: what hits the wall is not the server and not memory but the client — on port numbers. Measured: with a range of 50 numbers exactly 50 connections went through, and the fifty-first returned EADDRNOTAVAIL. The picture on a server reads the same way: thirty thousand sockets there say who closes the connections rather than how much load there is — measured, twenty identical exchanges left 20 sockets on the side that closed and 0 on the other.

Beyond that is what separates knowing from having read. The precise rule is about active close: on an ordinary close through FIN it is the actively closing side that goes through TIME_WAIT (there is also a simultaneous close and there is a reset, where the picture differs). The state lasts about a minute — measured, 60.4 seconds on a clock — and settings do not shorten it: tcp_fin_timeout applies to a different state and tcp_max_tw_buckets bounds the number of sockets rather than the duration. The port range divided by the duration of the state gives 467.7 connections per second — but that is a rough estimate for repeated connections to one and the same destination address and port in this configuration, not a ceiling of the machine. And none of it is cured by settings: measured, 400 requests through one connection spent one port and left one socket in TIME_WAIT.

Where to start
Before this lesson it is enough to understand
  • a client connects to a server, exchanges data with it and closes the connection;
  • a machine has an address, and a connection has a port number at each end;
  • a network is neither instant nor perfect: a packet can be delayed on the way or lost.
You do not need to know in advance
  • the TCP states by name, the order of packets on a close, what a FIN is;
  • ephemeral ports, ip_local_port_range, EADDRNOTAVAIL, tcp_fin_timeout, SO_REUSEADDR.

What this question is really about

The ladder looks like this:

  1. "What is TIME_WAIT?" — the warm-up about the state after a close.
  2. "Whose side does it appear on — the client's or the server's?" — the question that shows whether the rule about active close is known.
  3. "We have 30 thousand sockets in TIME_WAIT, what do we do?" — the central question, and the one where answering with settings is visibly wrong.
  4. "What does EADDRNOTAVAIL mean?" — a trap: it is not about memory and not about descriptors.
  5. "How many new connections per second will a client take?" — a question about arithmetic rather than about "it depends".
  6. "What is the state for in the first place?" — a question about what it protects.

The numbers come from running bench/timewait/ports.py and bench/timewait/practice.py over loopback. The duration of the state here is not taken from documentation but read off a clock.

Base: a connection leaves a trace behind after it closes

Before talking about states and port numbers it is worth naming, in ordinary words, what happens to a connection from start to finish. In short, its life goes like this:

  1. the two sides connect — they agree that they are talking to each other;
  2. the connection works — data travels over it in both directions;
  3. it is closed — one side says "that is all from me" and the other answers in kind;
  4. a waiting state — the conversation is over, but a trace of it is still held.

The whole lesson is about the fourth step. It has a name: TIME_WAIT.

Why that step exists. A network makes no promise that every packet arrived or that none of them was delayed on the way: a copy of a segment can wander across routes and turn up late. And a connection is identified not by a name but by four numbers — an address and a port on one side, an address and a port on the other. So if a connection is closed and a new one is opened straight away with the very same four, a late segment of the old conversation arrives into the new one — and is taken for its own, because there is nothing to tell them apart by.

The waiting state is the defence against exactly that: the side that closed does not hand its half of the four to anyone for a while. It waits until nothing of the old conversation can still be in the network.

That is already enough to answer the basic interview question: TIME_WAIT is not a leak and not a fault but the normal end of a connection. Beyond it comes which side gets the wait, how long it lasts, why a client runs out of port numbers because of it, and at what rate of connections that turns into a failure.

Mechanism 1: the wait goes to the side that closes actively

The first thing that settles half the questions in this topic: TIME_WAIT does not "appear in the system", it goes to a particular side of the connection. Which one is visible in a measurement where exactly one thing changes.

1. TIME_WAIT BELONGS TO WHOEVER CLOSED FIRST
--------------------------------------------
  the client closes first                        server side 0, client side 20
  the server closes first                        server side 20, client side 0
measured observationbench/timewait/ports.py. Both ends of the connection live in one process on loopback, so the ss output shows which of them the TIME_WAIT appeared on: on one side the server's port is the local address, on the other it is the peer's.

Twenty identical exchanges either way. The same server, the same client, the same data. The only difference is who called close first — and the sockets land entirely on that side.

Why that side and not the other. This is not an implementation quirk but a requirement of the standard:

When a connection is closed actively, it MUST linger in the TIME-WAIT state for a time 2xMSL (Maximum Segment Lifetime)

RFC 9293

The passive side has nothing to wait for: there is nothing left for it to acknowledge.

language contractWho gets the state is set by RFC 9293. The measurement shows that this is what happens, but the rule comes from the standard rather than from the run.

And here the rule is worth stating precisely, because the usual phrasing — "whoever closed first gets it" — is not always true. The standard is about active close, and on an ordinary close, where one side sends a FIN and the other answers with its own, that is the same thing as closing first: the measurement above was made exactly that way. But an ordinary close is not the only kind. There is a simultaneous close, where the two FINs cross on the way — then both sides close actively and both have to wait. And there is a reset, an abrupt tear-down instead of a FIN — a connection ended that way does not go through the waiting state at all. Neither is in this run: it shows the ordinary case. So the rule reads: the side performing the active close is the one that goes through TIME_WAIT, and "whoever closed first" is the special case of that for an ordinary close through FIN.

This is the practical point behind the question: thirty thousand TIME_WAIT on a server is not a complaint about clients but a fact about who closes connections. A server that answers and closes immediately accumulates them by construction. A server that keeps the connection open and lets the client close does not accumulate them at all.

The converse holds too: if the sockets pile up on the client, the client is the one closing actively — and the question is no longer "how do we remove them" but "why is there a new connection per request".

Mechanism 2: the duration is not tunable

The second mechanism is about what gets done instead of fixing the cause. How long the state actually lives:

2. HOW LONG IT LASTS
--------------------
  in TIME_WAIT right after close                 True
  seconds until it disappeared                   60.4
  tcp_fin_timeout on this machine                60
  tcp_max_tw_buckets on this machine             32768
  tcp_tw_reuse on this machine                   2
measured observationbench/timewait/ports.py. The duration is not taken from documentation: the script closes a connection and polls ss until the socket disappears. Hence the fractional number — it is a clock reading.

Sixty seconds on a clock. And that is where the trap is: tcp_fin_timeout sits right next to it at 60, and the resemblance looks like an explanation. It is not one — tcp(7) describes that setting differently:

This specifies how many seconds to wait for a final FIN packet before the socket is forcibly closed.

tcp(7), tcp_fin_timeout

Waiting for the other side's FIN is a different state and a different situation. It has nothing to do with the minute that was measured.

The two other settings people turn in this topic also do something else. On tcp_max_tw_buckets:

The maximum number of sockets in TIME_WAIT state allowed in the system. This limit exists only to prevent simple denial-of-service attacks.

tcp(7), tcp_max_tw_buckets

That is a bound on the count, not the duration — the quotation finishes the thought itself: If this number is exceeded, the socket is closed and a warning is printed. Such a bound does not solve "we are out of ports": it trades one problem for another.

The third setting, tcp_tw_reuse, is taken up below alongside SO_REUSEADDR: it is not about duration but about whether a port may be taken early. The value 2 in the run is not "enabled twice": on this machine reuse is permitted only for loopback, which is where the measurement runs.

language contractWhat each of the three settings governs is described in tcp(7) rather than derived from the run.
implementation detail · Linux 6.18.44The duration itself is a constant in the Linux kernel rather than a setting: the measurement shows its value on this machine. The standard defines it differently — as twice the maximum segment lifetime.

The practical point: the right answer to "how do we shorten TIME_WAIT" is "you do not, and you do not need to". What is needed is different: stop creating that many connections, or stop being the side that closes actively.

Mechanism 3: what the client runs out of is port numbers

Now what the lesson exists for. What exactly runs out for a client that opens a connection per request:

3. WHAT THE CLIENT RUNS OUT OF IS PORT NUMBERS
----------------------------------------------
  port range for this block                      50000 50049, that is 50 numbers
  connections made                               50
  what stopped the loop                          EADDRNOTAVAIL
measured observationbench/timewait/ports.py. The exhaustion is produced not by load but by narrowing ip_local_port_range for the duration of the block: the failure arrives in seconds and does not depend on how many connections the rest of the system has open. The original range is restored right after.

Fifty numbers, fifty connections. Not forty-eight and not fifty-two: exactly as many as there are numbers. There is nowhere for the fifty-first to come from, because every previous number is still held by that same waiting state.

And note the error itself. What it means is spelled out in connect(2):

upon attempting to bind it to an ephemeral port, it was determined that all port numbers in the ephemeral port range are currently in use.

connect(2), EADDRNOTAVAIL

Neither memory, nor descriptors, nor a refusal by the server appears in that definition: what ran out is numbers. The diagnosis follows — seeing this in a client's log, do not go looking on the server.

language contractThe meaning of EADDRNOTAVAIL is set by connect(2). The measurement shows when the kernel returns it; what causes it comes from the document.

Mechanism 4: the ceiling is computed rather than guessed — but it is an estimate

Out of two measured quantities — the size of the range and the duration of the state — comes a number worth keeping in mind:

4. THE CEILING IS ARITHMETIC OVER THE TWO NUMBERS ABOVE
-------------------------------------------------------
  ip_local_port_range on this machine            32768 60999
  port numbers in it                             28232
  seconds a port stays in TIME_WAIT              60.4
  new connections per second, ceiling            467.7
measured observationbench/timewait/ports.py. The range is read from /proc and the duration is read off a clock; the last row itself is one divided by the other rather than a separate observation.

About four hundred and seventy new connections per second — that is the whole headroom of an ordinary machine with default settings, if every request opens a new connection to one and the same destination.

The number is more modest than it sounds, and that is its value: a service at five thousand requests per second exceeds it tenfold, and no amount of machine capacity changes that — what runs out is numbers, not resources.

And straight away, how far the number reaches, because it is not a law of nature. It is a rough estimate for repeated connections to one and the same destination address and port in this configuration, not a ceiling of the machine. It has three boundaries. First: the kernel tells connections apart by four values — address and port on both sides — so a second destination gets its own set of numbers and the total is higher. Second: both quantities in the division were taken here, on this machine; with a different port range the number is different, and it has to be recomputed rather than remembered as a constant. Third: it is an estimate for the worst case — a client that reuses nothing.

language contractThat a connection is identified by a pair of sockets — address and port on both sides — is a definition from the standard rather than an observation of this run: there is no second destination in it.

That is precisely why the estimate is useful: not as a number in the machine's specification but as the order of load at which the habit of "a connection per request" stops working.

Mechanism 5: reuse removes the arithmetic entirely

The last mechanism is the shortest, because there is nothing to explain.

5. A REUSED CONNECTION SPENDS ONE PORT, WHATEVER THE LOAD
---------------------------------------------------------
  requests sent                                  400
  local ports spent                              1
  TIME_WAIT sockets left behind                  1
measured observationbench/timewait/ports.py. The same machine. The server here keeps the connection open, and the port range is the original one rather than the narrowed one from the third block. One thing is counted: how many local ports 400 requests spent.

Four hundred requests, one port, one socket in TIME_WAIT at the end. The third block on the same machine failed at the fiftieth connection.

Same machine, same kernel. The third block ran with the range deliberately narrowed to fifty numbers — hence the failure at fifty. Reuse does not care about the size of the range: one port, whatever it is. One decision changed — keep the connection or close it. And that decision removes the ceiling arithmetic, the question about settings, and half the questions in the topic.

It also removes what the last two lessons were paying for: name resolution and the TLS handshake. One connection, one time each.

Deeper: the ports run out somewhere other than where the application runs

All the arithmetic above was computed for one machine: its own range of numbers, its own count of connections, its own error in its own log. In a real network that assumption often does not hold — and then the ports run out for someone other than whoever spent them.

Between the machines and the outside world there is usually a gateway that rewrites addresses: connections leave not with the address of the application's machine but with the gateway's. To sort the answers back to the right machines afterwards, the gateway has to tell those conversations apart — and it tells them apart the same way the kernel does, by the port number on its own side. So it hands out numbers from its range, out of the same limited field of numbers.

Hence the point: behind such a gateway the port numbers are shared across the whole network of machines. They are spent by everyone together and they run out for everyone at once. This is reasoning rather than a measurement: there is no second machine and no gateway in the run.

Two consequences worth keeping in mind. First, the absence of EADDRNOTAVAIL on the application's machine proves nothing: numbers there may well be free while connections still fail, because they ran out on the gateway and that is where the symptom comes from. Second, computing the estimate for one machine understates the risk, because behind a shared gateway the load of the machines adds up into one count rather than splitting into independent ones.

The cure, however, is the same one, and that is the good news: a connection that lives and gets reused spends no number, neither on the machine nor on the gateway.

How to answer in an interview

Short answer: TIME_WAIT is the normal end of a connection rather than a leak. The side that closed holds on to its address-and-port pair for a while, so that delayed segments of the old connection are not accepted by a new connection with the same addresses and ports. It becomes a problem for a client that opens a connection per request: what runs out is not memory and not descriptors but port numbers. Measured: with a range of 50 numbers, exactly 50 connections and EADDRNOTAVAIL on the fifty-first.

That is enough to answer correctly. Beyond it is what to add if the interviewer digs.

If the interviewer digs deeper

Three things separate a good answer. First, you give an order of magnitude rather than "it depends": the port range divided by the duration of the state is about 470 new connections per second — and you name the boundary in the same breath, that this is a rough estimate for repeated connections to one and the same destination address and port in one particular configuration, not a ceiling of the machine. Second, you do not propose turning settings but explain why they are about something else: tcp_fin_timeout applies to a different state and tcp_max_tw_buckets bounds the number of sockets rather than the duration. Third, you say that TIME_WAIT on a server means the server is closing, which is a separate conversation about why it does not keep connections.

And one phrasing worth correcting in your own head beforehand. "It goes to whoever closed first" holds for an ordinary close through FIN, but the standard's rule is stated differently: the side performing the active close is the one that goes through TIME_WAIT. On a simultaneous close both sides close actively, and a connection ended by a reset does not go through the state at all.

What not to say: "we lowered tcp_fin_timeout and it got better". If it got better, that was not the reason.

Next they ask

Next they ask

What is the state for in the first place?

Short answer

So that an address-and-port pair is not reused while packets from the old connection may still be travelling. Otherwise a delayed packet arrives at a new connection with the same numbers and is taken for its own.

The standard says so directly: TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP peer received the acknowledgment of its connection termination request and to avoid new connections being impacted by delayed segments from previous connections.

Hence the duration: the standard defines it as twice the maximum segment lifetime. Linux does not compute it that way but holds a fixed minute — the one measured here. It cannot be shortened, not because the setting is hidden but because there is no such setting.

Next they ask

Does SO_REUSEADDR help?

Short answer

Not with this. socket(7) describes it as Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses — that is, it is about bind: about a server taking its listening port again after a restart while old connections are still in TIME_WAIT. It has nothing to do with a client's outgoing connections: their port number is chosen by the kernel, and the kernel still has nothing to choose from.

The setting that is about this is tcp_tw_reuse, and tcp(7) describes it carefully: allow reuse "when it is safe from protocol viewpoint", and do not change it without expert advice. That is a workaround, not a fix.

Next they ask

What about widening the port range?

Short answer

It works and is sometimes justified: the range read from /proc holds 28,232 numbers out of the 65,535 a port number can take — less than half the field — and widening it raises the ceiling proportionally. But raises — not removes: twice the ports give twice the ceiling, and that is all.

Compare with the previous block: reusing a connection changes the count not by a factor of two but by orders of magnitude — four hundred requests through one port. Widening the range is worth it where there is nothing to reuse: a proxy, say, which by its nature opens an outgoing connection per incoming one.

Next they ask

How does this look from a load balancer?

Short answer

The same, except it happens on both sides at once. A load balancer is a client to the servers behind it, so port exhaustion reaches it first and shows up as mysterious failures under load that appear on none of the servers.

Hence the first thing to check in that situation: does the load balancer keep connections to the servers open. If it opens an outgoing connection per incoming one, its ceiling is the one estimated in Mechanism 4, and it is lower than people expect.

Common misconceptions

Claim

Many sockets in TIME_WAIT means a leak

Actually

It is the normal state of a closed connection, and it goes to the side performing the active close. Measured: twenty identical exchanges left 20 sockets on the side that closed and 0 on the other. The number says nothing about a leak; it says how many connections you close and how fast.

Claim

The duration of TIME_WAIT is shortened by tcp_fin_timeout

Actually

That setting is about a different state: "This specifies how many seconds to wait for a final FIN packet before the socket is forcibly closed". Measured: the state lived 60.4 seconds on a clock. Its resemblance to the setting's value of 60 is a coincidence, not an explanation.

Claim

EADDRNOTAVAIL means running out of memory or descriptors

Actually

It is running out of port numbers, and that is how connect(2) defines it: upon attempting to bind it to an ephemeral port, it was determined that all port numbers in the ephemeral port range are currently in use. Measured: with a range of 50 numbers exactly 50 connections went through, and the fifty-first got this error.

Claim

The problem is on the server, since TIME_WAIT shows up on the server

Actually

It shows up wherever the active close happens. Measured: when the server closed first all 20 sockets landed on its side; when the client did, all 20 landed on the client. Sockets on the server mean the server is closing the connections, and the conversation is about why it does not keep them.

Claim

TIME_WAIT always goes to whoever closed first

Actually

That is the special case for an ordinary close through FIN — and that is how the measurement was made. The rule itself is stated from the active close: When a connection is closed actively, it MUST linger in the TIME-WAIT state. On a simultaneous close, where the two FINs cross on the way, both sides close actively; and a connection ended by a reset does not go through the state at all.

Claim

The ceiling on new connections is set by the CPU

Actually

Port arithmetic is hit sooner. Measured and computed: a range of 28,232 numbers divided by 60.4 seconds of state gives 467.7 new connections per second. That is not a ceiling of the machine, though: it is a rough estimate for repeated connections to one and the same destination address and port in this configuration — a second destination gets its own set of numbers, and a machine with a different range gives a different figure. The order of magnitude stands: a service at five thousand requests per second exceeds it tenfold, and machine capacity has nothing to do with it.

Claim

Ports are free on the application's machine, so ports are not the problem

Actually

They are free where you are looking. Outbound connections often leave through a gateway that rewrites addresses, and the only thing it has to tell those conversations apart by is a port number on its own side — out of its own limited range, shared across the whole network of machines behind it. They are spent by everyone together and run out for everyone at once. There is no measurement of this in the lesson: it follows from the port number on the gateway's side being all it has to distinguish connections with.

Practice

Two exercises. Answer first, then check against the real output: in both, the correct answer is what the measurement script prints.

Practice · predict the output

Twenty identical exchanges, the client closes first. Three things are printed: how many TIME_WAIT sockets ended up on the server side, how many on the client side, and how an attempt to open two hundred connections with a range of twenty port numbers ended. What does this code print?
server_side, client_side = who_holds(closes_first=False)
made, stopped = until_ports_run_out()
print(server_side)
print(client_side)
print(stopped)

Practice · estimate

The ephemeral port range holds 28,232 numbers and TIME_WAIT keeps a port for about a minute. How many new connections per second to one address will a client take if it reuses nothing?
connections per second

Knowledge check

Question 1 of 5

Whose side does TIME_WAIT appear on?

Sources & further reading

3 SOURCES

  1. tcp(7), Linux man-pages 6.7Official documentation. On the three settings people turn instead of fixing the cause. tcp_fin_timeout is not about TIME_WAIT: "This specifies how many seconds to wait for a final FIN packet before the socket is forcibly closed". tcp_max_tw_buckets bounds the count rather than the duration: "The maximum number of sockets in TIME_WAIT state allowed in the system. This limit exists only to prevent simple denial-of-service attacks". tcp_tw_reuse permits reuse rather than shortening the wait: "Allow to reuse TIME_WAIT sockets for new connections when it is safe from protocol viewpoint. It should not be changed without advice/request of technical experts".https://man7.org/linux/man-pages/man7/tcp.7.html
  2. RFC 9293, Transmission Control Protocol (TCP)Source. Where it is established who gets the state and what it is for: "When a connection is closed actively, it MUST linger in the TIME-WAIT state for a time 2xMSL (Maximum Segment Lifetime)". And the definition of the state itself: "TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP peer received the acknowledgment of its connection termination request and to avoid new connections being impacted by delayed segments from previous connections".https://www.rfc-editor.org/rfc/rfc9293.html
  3. connect(2) and socket(7), Linux man-pages 6.7Official documentation. What the error the client hits actually means. connect(2), EADDRNOTAVAIL: "upon attempting to bind it to an ephemeral port, it was determined that all port numbers in the ephemeral port range are currently in use". And what the setting people reach for instead is about — socket(7), SO_REUSEADDR: "Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses".https://man7.org/linux/man-pages/man2/connect.2.html