Skip to main content

⚙️ 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.

flowchart LR R["Incoming requests"] --> Q["Event queue"] Q --> L{"Event loop (1 thread)"} L -->|"needs I/O"| IO["Offload: DB / network / file"] IO -->|"done -> callback"| Q L -->|"ready work"| CB["Run callback, send response"]

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.

flowchart TD C["Incoming requests"] --> S["WSGI / ASGI server (e.g. Gunicorn / Uvicorn)"] S --> W1["Worker process 1"] S --> W2["Worker process 2"] S --> W3["Worker process 3"] W1 --> T1["Threads / async tasks"] W2 --> T2["Threads / async tasks"] W3 --> T3["Threads / async tasks"]

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.

flowchart LR R["Incoming requests"] --> WS["Web server (Nginx / Apache)"] WS --> FPM["PHP-FPM pool"] FPM --> P1["Worker: fresh state -> run -> discard"] FPM --> P2["Worker: fresh state -> run -> discard"] FPM --> P3["Worker: fresh state -> run -> discard"]

💡 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:

Which runtime model fits which workload Node shines at real-time and high-concurrency I/O; Python shines at mixed and compute-plus-data workloads with multiple processes; PHP shines at classic request-response web pages and content sites. Node.js Real-time / WebSockets Many concurrent I/O Streaming APIs Event loop Python Mixed CPU + I/O Data / ML workloads Async APIs (ASGI) Multi-process + GIL PHP Classic request/response CMS & content sites Simple horizontal scaling Shared-nothing / FPM
Figure 1 — Each runtime model has a workload it was practically designed for. Fit the workload to the model and you get performance almost for free.

💡 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.

  1. A live sports scoreboard pushing updates to 50,000 connected browsers.
  2. A marketing site with articles and a contact form, on a tight hosting budget.
  3. An API that runs a machine-learning model to score each uploaded image.
  4. 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
  1. Node — massive concurrent, I/O-bound live connections are the event loop's sweet spot.
  2. PHP — classic request/response content with cheap shared hosting; shared-nothing scales simply.
  3. Python — CPU-bound ML work fits its multi-process model and its unrivaled ML libraries.
  4. 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

🚀 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.