⚙️ How Each Language Runs on the Server
Two backends can look identical in your editor yet behave completely differently under load — because underneath, Node.js, Python, and PHP execute code in fundamentally different ways. This lesson opens the hood on all three so you can predict how each one handles concurrency, holds state, and scales.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain Node's single-threaded event loop and non-blocking I/O
- Describe Python's WSGI/ASGI models, threads vs async, and the role of the GIL
- Describe PHP's shared-nothing, process-per-request model and PHP-FPM
- Predict when each model shines or struggles under different workloads
- Explain the implications for state and performance in each
Estimated Time: 30–40 minutes • Difficulty: Intermediate
Hands-on: Match real-world workloads to the runtime model that fits them best.
In This Lesson
Why the Runtime Matters
The runtime model is how a language turns incoming requests into work and manages doing several things "at once." It's invisible while traffic is light — but it dictates how your app behaves when a thousand users arrive at the same moment, whether you can keep data in memory between requests, and where your performance ceiling sits.
💡 Two kinds of work. Keep this distinction in mind throughout: I/O-bound work waits on something external (a database, a network call, a file) — the CPU is mostly idle. CPU-bound work keeps the processor busy (image resizing, cryptography, big calculations). The three models handle these two cases very differently.
📖 Key Terms
Concurrency: making progress on many tasks over the same period (not necessarily at the same instant).
Parallelism: literally running tasks at the same instant, on multiple CPU cores.
Blocking: a line of code that stops everything else until it finishes (e.g. a slow, synchronous database read).
Node.js — The Event Loop
Node runs your JavaScript on a single main thread driven by an event loop. Instead of blocking while it waits for slow I/O, Node hands that work off (to the OS or a background thread pool) and immediately moves on to the next task. When the I/O finishes, a callback is queued and the loop picks it up. One thread stays busy juggling thousands of in-flight operations.
This is brilliant for I/O-bound workloads: while one request waits on the database, the single thread happily serves hundreds of others. The catch is the flip side.
⚠️ Don't block the loop
Because there's one thread, a heavy CPU-bound task (say, resizing a large image synchronously) freezes every other request until it finishes. Node's answer is worker threads or offloading heavy compute to separate services — but it's deliberate extra work, not the default.
✅ In one line
Node = one thread + non-blocking I/O. Superb for many concurrent, I/O-bound connections; awkward for heavy CPU work.
Python — WSGI, ASGI & the GIL
Python web apps historically run behind WSGI (Web Server Gateway Interface) — the synchronous standard used by Django and Flask. A WSGI server (like Gunicorn or uWSGI) runs multiple worker processes, and each worker can handle requests using threads. Newer frameworks like FastAPI use ASGI, the asynchronous successor, which adds an event loop of its own for high-concurrency async I/O.
The twist is the GIL (Global Interpreter Lock). In the standard CPython interpreter, only one thread can execute Python bytecode at a time within a single process. So threads give you great concurrency for I/O-bound work (a waiting thread releases the GIL), but they do not give you true multi-core parallelism for CPU-bound Python code.
💡 How Python gets multi-core anyway
By running multiple processes — each with its own interpreter and its own GIL. That's exactly why WSGI/ASGI servers spawn several worker processes (a common rule of thumb is roughly 2 × CPU cores + 1). Async (async/await) additionally lets a single worker juggle many I/O-bound requests without threads.
✅ In one line
Python = multiple worker processes, each with threads and/or an async loop; the GIL means you scale CPU work with processes, not threads.
PHP — Shared-Nothing per Request
PHP's model is the simplest to reason about: every request starts with a completely clean slate. A request comes in, PHP boots up, runs your script from top to bottom, sends the response, and then throws everything away — variables, state, all of it. Nothing is shared between requests. This is the shared-nothing architecture.
In production this is powered by PHP-FPM (FastCGI Process Manager), which keeps a pool of PHP worker processes ready. The web server (Nginx or Apache) hands each incoming request to a free worker; that worker runs the request in isolation and returns to the pool when done.
💡 Why this is a feature, not a bug
Because state never leaks between requests, whole classes of concurrency bugs simply can't happen. It's incredibly robust and easy to scale horizontally — just add more workers or servers. The cost is that PHP isn't a natural fit for persistent connections (like WebSockets), since each request tears everything down. Extensions like Swoole and Laravel Octane add long-lived processes when you need them.
✅ In one line
PHP = process-per-request, shared-nothing. Bulletproof isolation and easy scaling; not built for long-lived, real-time connections.
Implications for State
The runtime model directly shapes how — and whether — you can hold data in memory between requests.
| Question | Node.js | Python | PHP |
|---|---|---|---|
| Keep data in memory across requests? | Yes — one long-lived process | Yes — but per worker process | No — wiped after each request |
| Natural fit for WebSockets / real-time? | Excellent | Good (with ASGI) | Needs extra tooling (Swoole/Octane) |
| Risk of shared-state concurrency bugs? | Possible (shared memory) | Possible (within a worker) | Very low (nothing shared) |
| Where shared state usually lives | In-process or Redis | Redis / external cache | Redis / DB / external cache |
⚠️ The portable rule
Regardless of language, don't rely on in-process memory as your source of truth in production — you'll usually run multiple processes or servers behind a load balancer. Put shared state in an external store like Redis or your database. This keeps your app correct no matter which runtime model it's on.
When Each Model Shines
Tie it together with the kinds of work each model was built to handle well:
💡 The honest reality
Most typical CRUD web apps are I/O-bound, and all three models handle that well. The runtime model only becomes a deciding factor at the extremes — massive real-time concurrency (favors Node), heavy in-process compute or data/ML (favors Python's multi-process approach), or dead-simple content delivery at scale (favors PHP).
Hands-on Exercise
🏋️ Match Workload to Model
Objective: Reason from a workload's shape to the runtime model that fits it.
Instructions:
For each scenario below, decide which model (Node event loop, Python multi-process, or PHP shared-nothing) fits best and write one sentence explaining why, using the I/O-bound vs CPU-bound distinction.
- A live sports scoreboard pushing updates to 50,000 connected browsers.
- A marketing site with articles and a contact form, on a tight hosting budget.
- An API that runs a machine-learning model to score each uploaded image.
- A chat app where messages must appear instantly for everyone in a room.
💡 Hint
Ask two questions per scenario: (1) Does it need persistent, real-time connections? (2) Is the heavy work I/O-bound (waiting) or CPU-bound (computing)? Those two answers point straight at a model.
✅ Example answers
- Node — massive concurrent, I/O-bound live connections are the event loop's sweet spot.
- PHP — classic request/response content with cheap shared hosting; shared-nothing scales simply.
- Python — CPU-bound ML work fits its multi-process model and its unrivaled ML libraries.
- Node — persistent, real-time messaging via WebSockets is exactly what the event loop excels at.
🎯 Quick Quiz
Question 1: Why can a heavy CPU-bound task be dangerous in a default Node.js server?
Question 2: How does CPython typically achieve true multi-core parallelism despite the GIL?
Question 3: What best describes PHP's classic "shared-nothing" model?
Summary & Quiz
🎉 Key Takeaways
- Node.js — one thread + an event loop; excellent for concurrent I/O, but CPU-bound work blocks everything.
- Python — WSGI/ASGI servers run multiple worker processes; the GIL means CPU parallelism comes from processes, not threads.
- PHP — shared-nothing, process-per-request via PHP-FPM; ultra-robust and easy to scale, but not built for persistent connections.
- The runtime model dictates whether you can hold state in memory — put shared state in Redis or the database to stay portable.
- Most CRUD apps are I/O-bound and fine on any model; the model matters most at the extremes.
📚 Further Reading
- Node.js — The Event Loop
- Python Docs — Global Interpreter Lock
- PHP Manual — FastCGI Process Manager (FPM)
- ASGI Specification
🚀 What's Next?
With the comparative foundation set — how to choose, how the syntax lines up, and how each language runs — we now go deep on the first of the three. Next up: Node.js architecture and its ecosystem in detail.
🎉 Nice work!
You can now predict how a backend behaves under load before writing a line of it. Time to go deep on Node.js.