Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

Processes vs Threads vs Async: How Servers Handle Many Users at Once

About Post

A thousand people hit your site at the same moment. Your server has a handful of CPU cores. Somehow, most of them get a response in well under a second.

How? The answer depends on your stack, and it's different for PHP, Node.js, Java or Go. Understanding it explains a lot of things that otherwise feel random: why one slow API call can take down a PHP site, why one heavy loop can freeze a Node app for everyone, and why "just add more workers" sometimes makes things worse.

The restaurant kitchen

Picture a kitchen taking orders. There are three classic ways to run it.

Processes: many separate kitchens. Each order gets its own small kitchen with its own cook, fridge and tools. Nothing is shared, so one cook's mess never spills into another's. But kitchens are expensive, and you can only fit so many in the building.

Threads: many cooks in one kitchen. Several cooks share one kitchen, one fridge and one set of knives. Cheaper than separate kitchens, and they can hand things to each other. But two cooks can reach for the same pan at the same moment, so they need rules about who touches what.

Async: one very organised cook. One cook puts a pot on to boil, starts the oven, takes the next order, and checks back when a timer rings. She never stands watching water boil. She's brilliant at juggling lots of waiting. But if one order needs ten minutes of non-stop chopping, every other order waits.

Those are processes, threads and an event loop. Now let's see who uses which.

PHP-FPM: a pool of processes

A classic PHP app behind Nginx or Apache runs on PHP-FPM, which keeps a pool of worker processes. Each worker handles one request at a time, from start to finish, then takes the next one. Every request starts with a clean slate: Laravel boots, handles the request, and everything is thrown away.

That model is wonderfully simple. A memory leak or a crash affects one request, not the whole server. There's no shared state between requests to corrupt.

The catch is the pool size. It's set in the FPM config (example values):

pm = dynamic
pm.max_children = 20      ; hard limit on parallel requests
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500     ; recycle workers to contain leaks

If all workers are busy, new requests queue. Now imagine your app calls a slow payment gateway or a third-party API that takes several seconds to reply. Every worker waiting on that call is doing nothing, but it's still occupied. A few seconds of slowness from a partner can use up the whole pool, and suddenly unrelated pages time out.

The fixes: set timeouts on every outgoing HTTP call, move slow work to queued jobs, and size pm.max_children from memory, not hope. A rough guide is the RAM you can give PHP divided by the memory one worker uses. Setting it higher than that just swaps "slow" for "server out of memory".

Node.js: one event loop

Node runs your JavaScript on a single thread with an event loop. When code makes a database query or an HTTP call, Node doesn't wait. It registers a callback (or a promise), moves on to the next request, and comes back when the result is ready. One process can juggle a very large number of connections, as long as they're mostly waiting on I/O.

Under the hood there's a small thread pool too (libuv's, four threads by default) for things the operating system can't do asynchronously, like some file system work, DNS lookups and certain crypto functions. But your JavaScript runs on one thread.

Which is exactly the catch. CPU work blocks everyone:

app.get('/report', (req, res) => {
  // ❌ synchronous file read + heavy loop: every other user waits
  const rows = JSON.parse(fs.readFileSync('payments.json', 'utf8'));
  res.json(buildYearlySummary(rows));
});

app.get('/report', async (req, res) => {
  // ✅ non-blocking I/O; heavy CPU work moved to a worker or job
  const rows = JSON.parse(await fs.promises.readFile('payments.json', 'utf8'));
  res.json(await runInWorker('yearly-summary', rows));
});

(runInWorker stands in for your own helper around worker_threads or a job queue.) The rule in Node: never block the loop. Use async APIs, and push CPU-heavy work like image processing, PDF generation or big calculations to worker threads or a background queue. To use all your CPU cores, run several Node processes, with a process manager like PM2, cluster mode, or multiple containers.

Threads: the Java, .NET and Go way

Many other platforms handle each request on its own thread inside one long-running process. Threads are lighter than processes and share memory, so caches, connection pools and configuration are loaded once and used by everyone.

The price is that shared memory brings back the race conditions and locks we'd rather not think about. And traditional OS threads aren't free either, so very high concurrency used to need careful tuning. Modern runtimes soften this: Go's goroutines and Java's virtual threads are very lightweight, letting you write simple blocking-style code that the runtime schedules efficiently.

Side by side

Processes (PHP-FPM)Event loop (Node.js)Threads (Java, Go, .NET)
Requests per workerOne at a timeMany, interleavedOne per thread, many threads
Shared memoryNone between requestsEverything, one threadEverything, many threads
Great atIsolation, simplicityLots of I/O waitingMixed I/O and CPU work
What hurtsSlow external calls tie up workersCPU work blocks everyoneRaces, locks, tuning
Scale up byMore workers, more serversMore processes, more serversMore threads, more servers

PHP is getting a bit of everything

PHP isn't stuck with one model. Laravel Octane runs your app on long-lived servers like FrankenPHP, Swoole or RoadRunner, so the framework boots once and stays in memory. That removes the boot cost from every request, but it also brings back a threads-style risk: state you leave in a static property or a singleton can leak into the next user's request. PHP 8.1's Fibers and libraries like ReactPHP and Amp make event-loop style code possible too.

For most Laravel apps, though, plain PHP-FPM with sensible timeouts and queued jobs scales a long way. The model's simplicity is a feature.

The one thing to remember: every model has a resource it runs out of. PHP-FPM runs out of workers, Node runs out of event-loop time, threaded servers run out of threads or memory. Know which one your stack has, and protect it: timeouts for waiting, queues for heavy work.

A quick diagnosis guide

  • PHP site slows down when a partner API is slow: workers are stuck waiting. Add timeouts, move the call to a job.
  • Node app freezes for everyone during one heavy request: the event loop is blocked. Find the sync or CPU-heavy code.
  • Threaded app has rare, unreproducible data bugs: look for shared mutable state.
  • Octane app shows one user's data to another: state is surviving between requests.

Which model does your main stack use, and which of these failure modes have you actually met in production?

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close