The 3% Drop That Only Showed Up on a Clean Server: A Debugging Retrospective A developer traced a 3% message loss in a TCP forwarder to a read loop that replied once per read instead of per line, a bug that only surfaced under burst load. The fix was a twelve-line change, and the developer recommends burst-mode testing to expose pipelining issues that polite tests miss. My forwarder lost 3% of its messages. My laptop reported zero. CI reported zero. Staging lost thousands every hour. I blamed the network. I blamed the VM. I blamed everything except my read loop. This is the retrospective: one wrong assumption, three counters, a twelve-line fix. The service was a small TCP forwarder. Accept a connection, read text lines, forward them. Nothing exotic. Staging showed the loss: 97,004 lines in, 94,183 lines out. No errors. No exceptions. Just missing messages. Replays on my laptop always passed. 100,000 lines in, 100,000 lines out. That mismatch is the first clue. I was not testing what production actually did. When a bug only lives in one environment, do you fix the environment first? Or the assumption your tests never stressed? Before touching code, I changed only the machine. Same binary, same load script, same counters. I started a fresh server from the free server option of MonkeyCode, an open source AI coding project. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The loss reappeared. 100,000 lines in, 97,128 lines out. The environment changed the probability, not the existence of the bug. A second machine is a microscope for timing bugs. A fresh one has no history and no bias. Logs tell stories. Counters add up. I placed a counter at every layer. Run once, compare four numbers. The gap between them names the losing layer. | Counter gap | Suspect | |---|---| | parsed < sent | read loop or buffer size | | replies < parsed | reply logic this bug | | received < replies | client read logic | The clean-server run was loud: sent 100,000, parsed 100,000, replies 99,987, received 99,987. The server parsed everything. It just did not reply enough. That gap is the fingerprint. Smallest code that fails. One socket, one buffer, no frameworks. load.py - burst mode, no waiting between sends: python load.py - burst mode. No waiting between sends. import socket N = 100 000 s = socket.create connection "127.0.0.1", 9000 s.settimeout 5 acks = 0 try: for in range N : s.sendall b"PING\n" while acks < N: data = s.recv 65536 if not data: break acks += data.count b"\n" except TimeoutError: pass print f"sent={N} acks={acks} lost={N - acks}" bad server.cpp - one reply per read, not per line: // bad server.cpp - one reply per read, not per line. include