One Slow Connection Froze Every Other One
Not everything you send goes out
In one line
recv and send do not promise the size you asked for. Holding on to what is left is the program's job.
Why this was needed
Once you switch to multiplexing, a new kind of bug appears. In blocking mode, sendall did not return until everything was sent, on its own. The moment you switch to non-blocking, that convenience is gone. If the kernel's send buffer has room for only 20 bytes and you try to send 200 bytes, send returns 20 and leaves the rest to the program.
The description of the return value in send(2) is short. On success, it returns the number of bytes sent. It says the number of bytes sent, not the number of bytes you tried to send. If you skim past this one line, you end up with a program that runs fine normally but has its tail end cut off only when the response gets bigger or the other side gets slower. And that symptom is almost always reported first as an error in the other side's parser.
How it works
Both the receiving side and the sending side need a place per connection to hold the leftover pieces. The receive-side buffer is the same story as the framing covered in the previous course. The protocol in this course is line-based, so you only need to collect until a newline appears, but a missing newline does not make the input wrong. It just hasn't fully arrived yet.
The send-side buffer is the part you are learning fresh. Each connection holds the bytes that could not go out yet under a name like outbox, calls send once when write readiness arrives, and cuts off from the front only as many bytes as the returned count.
sent = sock.send(self.outbox)
self.outbox = self.outbox[sent:] # 나머지는 남긴다
A common mistake here is self.outbox = b"". It passes when you test with short strings, since they usually go out in one go, and it breaks only in production when the response gets bigger.
Now it matters when you change the interest events. Read readiness is true only when the other side has sent something, but write readiness is true when the send buffer has room. Most of the time the buffer is empty, so write readiness is almost always true. If you watch for write readiness when there is nothing to send, the multiplexing call returns immediately with nothing to do, over and over.
| State | Events to watch | Reason |
|---|---|---|
| Nothing to send | Read only | Write readiness is almost always true, so the loop spins idle |
| Something left to send | Read and write | You need to know the moment room appears so you can keep sending |
So the rule boils down to one line. Watch for write readiness only while something is left to send. If you recompute the interest events at two moments, right after putting something in the queue and right after finishing sending, this rule is kept.
The receiving side has a matching rule. It is whether to call recv only once per read readiness, or until EAGAIN occurs. With level triggering, the default behavior of selectors, you are woken again next time if data is left, so calling it only once is safe. Calling it only once is also better for fairness between connections. That is because one chatty client cannot monopolize the loop in a single wake-up.
Here you may recall the edge triggering that epoll(7) describes. Edge triggering notifies only at the moment the state changes, so if you don't read until there is nothing more to read on a wake-up, the leftover data falls asleep waiting for the next notification. Level triggering, on the other hand, keeps notifying while the condition holds, so even if you read too little, it is not forgotten. The two are not a matter of performance superiority but of where the responsibility lies. If you paste code over without knowing which one you use, it becomes the kind of bug where responses occasionally vanish and cannot be reproduced.
Buffers need upper bounds too. If the other side keeps sending bytes without a newline, the receive-side buffer grows without end. The send side is the same: if the other side doesn't read and you keep queueing responses, memory gets tied to that one connection. It means the length limits and backpressure you learned in the previous course are needed here just the same. Multiplexing only moves the waiting and does not remove it, and the moved waiting usually shows up in the form of memory.
What it looks like in the field
Partial sends usually hide well. If a response is smaller than the kernel send buffer, it always goes out in one go, so it passes tests and passes staging. The moments it shows up are set. When a list is attached and the response grows, when the other side slows down its reading, and when you push responses into one connection back to back. At that point the other side receives cut-off JSON or half a line, and the report comes in as a parser error. Only success is left in the sender's log.
Conversely, the incident that comes from not fixing the interest events arrives first on the CPU graph. One core is stuck at 100 percent regardless of the number of connections, and the profiler shows the multiplexing call itself as the hottest spot. In fact that call is not slow; it is being called too often with nothing to do.
What you will do in the next quiz
Work out how to handle each of these: the case where send returned 3 out of 20 bytes, the CPU graph of a server that always watches write readiness, and the case where you received a piece with no newline. All three are different faces of the same question, where do you keep what is left.