TCP and UDP/TCP, step by step/Finishing, half-close, reset
Lesson 10 of 12 · in 3D and VR

Finishing, half-close, reset

A TCP connection is two byte streams, one each way, and each is closed on its own. A FIN ends one direction politely, a RST throws the whole connection away, and the side that closed first keeps some state for a while.

Lonely BinaryUpdated 2026-10-014 min readNo board required

View it in VR

Lesson 10 of the TCP and UDP course opens in a VR headset, on a table in front of you, and a voice starts three seconds after you arrive. Type this short address into the browser on a headset such as Meta Quest or Apple Vision Pro, and press Enter VR. No headset? Press Start the lesson: the same lesson, full screen.

learn.lonelybinary.com/vr/tcp-udp/10

A TCP connection carries two byte streams, one in each direction, and each has its own sequence numbers. So each one ends on its own. A FIN is a flag on a segment that means "I have nothing more to send". It uses up one sequence number, like the SYN did, so it is acknowledged and, if lost, sent again like data.

Half-closed

The standard says the two directions are closed independently, and that a host may keep sending in the open direction after the other side has sent its FIN. The side that sent the first FIN waits in FIN_WAIT_2 once it is acknowledged, still reading. The side that received it sits in CLOSE_WAIT until its own program closes. Only when the second FIN is acknowledged is the connection finished.

In sockets this is shutdown(SHUT_WR). A plain close() is allowed to mean more: the standard lets a host treat it as "no more reading either", and a host that does so should answer with a RST if data is still pending.

Reset

A RST says the connection is gone and nothing on it will be honoured. A valid one aborts the connection at once, the program is told, and whatever the receiver had not yet read goes with it. It is sent when a program aborts, and when a segment arrives for a connection that does not exist: a port with no listener answers a SYN with a RST, which is why a refused connection fails at once instead of after a timeout. Everything before a FIN is delivered and can be read. A RST delivers nothing further.

TIME_WAIT

The side that closes first ends in TIME_WAIT and keeps the connection's state for twice the maximum segment lifetime, so that a repeated FIN can still be answered and old copies of segments die out before the same four numbers are used again. The standard takes the lifetime to be two minutes, "an engineering choice", so four minutes on paper. Linux holds sixty seconds, a constant in its source. lwIP, the stack on an ESP32, holds 2 × TCP_MSL, which is 120 seconds by default.

It is state, not a fault. A board that serves a page and closes the connection itself holds one of these for every page it has served, and lwIP can reclaim the oldest when it runs out of slots. Who closes first decides who pays that; serving a page from the board is where it shows.

What it costs

A FIN means the sender's bytes have ended and, once acknowledged, that the receiver's TCP has them; it does not mean the program at the other end has read or acted on them. And an abort loses data in a way that looks like nothing at all: the sender sees a clean write, and the receiver never sees the bytes.

Common mistakes

  • Closing while the other side is still sending. The close can turn into a RST, and what was in flight is lost.
  • Using a reset as a goodbye. It works, and it throws away whatever the other program had not read yet.
  • Treating TIME_WAIT as a leak. It is the normal state of the side that closed first.
  • Reading "closed" as "processed". A FIN ends a stream of bytes; it says nothing about what the program did with them.

Edit this page — content/fundamentals/tcp-udp/closing-a-connection.mdx

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse Fundamentals on the forum →