DEV Community

The Byte Mechanics
The Byte Mechanics

Posted on Edited on Originally published at Medium

One Bad Request Crashed Everyone: Processes vs Threads

Originally published on Medium.

A user uploads one corrupted image. Your server crashes. And every other user on it goes down too: their uploads, their checkouts, their sessions, all gone.

One bad request shouldn't be able to do that. But on a lot of servers, it can. Whether it does comes down to one choice: processes or threads.

This article explains both, the traps that come with each, and how real systems combine them. If you'd rather watch, here's the 8-minute animated version:


1. The problem: many requests at once

Hundreds of requests hit a server at the same time: uploads, logins, checkouts. Handle them one by one, and everyone waits, while the CPU sits idle every time a request waits on the disk or the database.

So the operating system gives us two ways to work on many requests at once: processes and threads.

2. A process is a house

A process is a running program with its own private world. The operating system gives it its own memory (code, data, heap and stack), its own ID, its own open files and its own permissions.

Think of a process as a house. Everything inside belongs to that house, and the neighbours can't walk in and move your furniture.

A process is a house: its own code, heap, stack, files and PID, walled off from neighbouring processes

This isolation is enforced by hardware. Each process sees its own virtual addresses, and the CPU translates them to real memory. Process A literally cannot point at Process B's memory. So one process can corrupt its own memory, freeze or crash, and the house next door doesn't notice.

3. Threads are the people inside

A thread lives inside a process. It's one path of execution, a sequence of instructions the CPU runs. Every process starts with one thread and can create more.

If the process is the house, threads are the people living in it. Each person has their own to-do list and remembers where they are in it: that's the thread's own stack and registers. But they share everything else: the same heap, the same global variables, the same open files.

That sharing is the whole point of threads. It's also the whole problem.

4. Why threads became the default

  • Cheaper to create. A new process needs a whole new memory space. Copy-on-write makes that cheaper, but it's still real work. A new thread only needs its own stack.
  • Cheaper to switch. Switching between threads of the same process keeps the same memory map. Switching between processes changes the map, and the CPU's address cache goes cold. One switch costs microseconds, and thousands per second add up.
  • Easier to share data. Processes must send messages through pipes, sockets or queues, and every message is copied. One thread can simply write a variable that another thread reads.

Fast, simple, no copying. But "any thread can change anything, at any time" is a dangerous sentence.

5. The trap: race conditions

Two threads each add one to a shared counter that starts at 100. You expect 102. But count = count + 1 isn't one step. It's three: read, add, write.

Thread A reads 100. Before it writes, the CPU switches. Thread B also reads 100, adds one and writes 101. Thread A resumes, adds one to its stale 100, and also writes 101. Two increments happened. One got lost.

Race condition: two threads read a shared counter of 100, both add one and write 101, so one update is lost

That's a race condition. It only shows up under load, disappears when you add a print statement, and never reproduces on your laptop. Now imagine the counter is an account balance, or the number of concert tickets left.

6. The fix, and the next trap

The standard fix is a lock (a mutex). A thread takes the lock before touching the counter, and everyone else waits until it's released. Read, add and write now happen as one unit.

But locks bring their own trap. Thread A holds lock 1 and waits for lock 2. Thread B holds lock 2 and waits for lock 1. Neither will ever let go.

Deadlock: thread A holds lock 1 and waits for lock 2 while thread B holds lock 2 and waits for lock 1, so everything stops

That's a deadlock. Nothing crashes. Everything just stops. Shared memory is fast, but every shared thing needs rules. Get them wrong, and you get lost updates or a frozen system.

7. Back to the corrupted image

Image decoders are often written in C. A broken file can make one touch invalid memory. When a thread does that, the operating system doesn't kill just that thread. It kills the whole process, and every other thread dies with it. That's how one upload took down 500 users.

Shared memory, shared fate: one thread touches invalid memory, the OS sends a segfault, and every thread in the server process dies

If each request were handled by a separate worker process, only that worker would die. A supervisor starts a fresh one, and nobody else notices.

Separate worker processes: the bad photo.jpg crashes only worker 3 while workers 1, 2 and 4 keep running

Chrome made this choice on purpose. Each site gets its own renderer process, so a crashing page only kills its own tab. It's a security win too: a malicious site in one process can't read your banking tab's memory. The price is RAM, which is why Chrome uses so much of it. Isolation costs resources.

8. Your workload and language matter

  • I/O-bound work (mostly waiting on databases, networks or disks): threads are great. While one waits, another runs.
  • CPU-bound work (image processing, number crunching): you want real parallel execution on every core.
  • Python: in standard CPython, the Global Interpreter Lock lets only one thread run Python code at a time. For CPU-heavy Python, use multiple processes, one per core.
  • Node.js: your JavaScript runs on a single thread with an event loop, and heavy work goes to worker threads or separate processes.

9. Real systems use both

  • PostgreSQL starts a process per connection. A failed query stays inside its own process, but every backend also shares one block of memory for cached data. So if one backend crashes outright, the postmaster restarts all of them to be safe. Processes are also heavy, which is why people put a connection pooler in front of it.
  • MySQL handles each connection with a thread inside one server process. Lighter per connection, with shared memory.
  • Nginx runs a few worker processes, usually one per core, each with an event loop juggling thousands of connections.
  • A typical Python web app runs several worker processes with threads or async inside each.

The pattern used by PostgreSQL, MySQL, Nginx and Python web apps: processes for isolation and every core, threads or async for cheap concurrency

The pattern is the same everywhere: processes for isolation and to use every core, threads or async inside them for cheap concurrency.

10. How to choose

How to choose: use processes when crashes must stay contained or you need security boundaries; use threads when tasks share lots of data

Reach for processes when a crash must stay contained, when you need security boundaries, or when your language can't run threads in parallel.

Reach for threads when tasks share a lot of data, when you need many lightweight workers, and when you're willing to protect shared state with care.

In production, expect both: a few processes, with many threads or async tasks inside. With every request in threads under one roof, one corrupted image took everyone down. With separate worker processes, one worker dies, restarts, and nobody else notices.


▶️ Watch the animated version: One Bad Request Crashed Everyone: Processes vs Threads (YouTube)

Next up: how async and event loops handle thousands of connections with a single thread. Watch One Thread, 10,000 Users.

If this helped, follow The Byte Mechanics here for a new visual deep dive every week.

About: The Byte Mechanics turns how software really works into short animated explainers: system design, backend and operating systems, one diagram at a time.

Top comments (0)