DEV Community

Cover image for The Bugs That Hide Behind "It Works": Debugging a Multithreaded C++ Proxy Server
Ace-2504
Ace-2504

Posted on

The Bugs That Hide Behind "It Works": Debugging a Multithreaded C++ Proxy Server

As a student, one of the biggest lessons I've picked up is that a program which compiles and runs is not the same as a program that is correct. It really hit home while I was building a multithreaded C++ network proxy server — a project that authenticates users with SHA-256, enforces role-based website filtering, and caches HTTP responses using a custom LRU cache.

The happy path worked on pretty early. The real learning started when I went looking for the bugs that don't announce themselves — the ones that only show up under concurrency, fragmented packets, or heavy load. Here are four of the most interesting and challenging issues I ran into and how I fixed them.

1. The Thread Pool That Never Detached

The proxy spawns 20 persistent worker threads at startup. The idea was simple: create the workers, then detach them from the main thread so they run independently.

The bug was an ordering mistake. My detachment loop ran before the threads were created:

for (auto& t : workers) t.detach();   // workers is still EMPTY here
workers.emplace_back(...);            // threads created afterward
Enter fullscreen mode Exit fullscreen mode

Because the vector was empty when the loop ran, the iteration did nothing — the threads were never actually detached. It's the kind of bug that compiles cleanly, passes a quick test, and then causes resource and lifecycle problems once the server runs for a while.

The fix: I changed the order of the code. First, I created all the worker threads. Then, I detached them. What I learned: when working with threads, the order of setup steps matters. It is part of making the program correct, not just a small coding detail.

2. A Data Race Hiding Inside the Logger

My logging system looked thread-safe. Every write to proxy.log was guarded by a std::lock_guard<std::mutex>, so 20 threads writing at once could never scramble the file.

But the timestamps came from ctime(&now). Under POSIX, ctime returns a pointer to a globally shared static buffer. My mutex protected the file stream — it did NOT protect that hidden global buffer. So two threads formatting timestamps at the same time could corrupt each other's strings, even though the file writes themselves were perfectly safe.

The fix: switched to the thread-safe version, ctime_r(), which writes into a buffer. The lesson was a subtle one: locking the obvious shared resource isn't enough. You also have to think about the shared state hiding inside the standard library functions you're calling.

3. The TCP Read That Assumed Too Much

My request handler made a single call:

recv(client_socket, buffer.data(), BUFFER_SIZE, 0);
Enter fullscreen mode Exit fullscreen mode

This assumes that one recv() gives you one complete HTTP request. TCP makes no such promise. TCP is a byte stream, not a message protocol — a request can arrive split across several packets. If a header got cut in half, my request.find("Host:") parsing would just fail, and a valid request would get dropped for no obvious reason.

The fix: replaced the single read with a loop that keeps calling recv() until the \r\n\r\n header terminator has fully arrived. For my proxy, this handled the request headers; a fuller HTTP implementation would also need to handle bodies using Content-Length or chunked transfer encoding. This turned out to be one of the most common (and most underestimated) mistakes in network programming: treating a stream like a neat sequence of separate messages.

4. Defending Against Hanging Connections

Worker threads are a limited resource — I only have 20. A single unresponsive remote server that never closes its connection could tie up a worker forever, and 20 of those stalls would quietly take the whole proxy offline.

My defense was setting a receive timeout with setsockopt() using SO_RCVTIMEO, set to one second. If a remote server goes quiet mid-response, the thread frees itself instead of hanging forever. Combined with proper HTTP status codes sent back to the client — 502 Bad Gateway when DNS resolution fails, 504 Gateway Timeout when the upstream stalls — the proxy fails gracefully instead of dying silently.

What Debugging Concurrent Code Taught Me

The common thread across all four bugs is the same: concurrency and networking break the assumptions that work perfectly in single-threaded, single-packet test runs.

  • Setup order is part of correctness, not a detail.
  • A mutex protects what it wraps — and nothing else, including hidden global state.
  • The network gives you a byte stream, never a tidy message.
  • Limited resources need timeouts, or one bad peer takes everything down.

None of these bugs threw an error or crashed the compiler. They lived in the gap between "it runs" and "it is correct" — and for me, closing that gap has been the most rewarding part of learning systems programming.

If you've built low-level networked systems in C++, I'd love to hear which subtle concurrency bug cost you the most time to track down.

Top comments (0)