Threading, speed under load, memory, and real-time capability are not four separate facts. Each one falls out of this single choice.

PHP vs JavaScript for web development, decided by execution model

Skip the feature checklist. PHP and JavaScript differ in one thing that causes everything else: PHP is share-nothing, one request per PHP-FPM worker; JavaScript on Node is one thread and one event loop that interleaves thousands of requests by never blocking on I/O. Threading, speed under load, memory, and real-time capability all fall out of that fork. Here is the model, the worked req/s numbers, and a per-layer way to choose.

Two execution models, one root cause#

Most write-ups on PHP vs JavaScript for web development arrive as a feature checklist. They compare syntax, list "PHP is server-side, JavaScript is client-side", and chart popularity. Yet none of that predicts how either one behaves under real load. The behaviour comes from one place, and both languages made an opposite choice there.

That place is the concurrency model. In short, how does the runtime handle a second request while the first is still waiting on a database? PHP answers with more processes. JavaScript answers with one process that never waits. Hold those two answers side by side and every downstream difference becomes predictable.

PHP: share-nothing, one request per worker#

PHP runs behind PHP-FPM, a pool of worker processes. Because each worker handles exactly one request at a time (see the PHP-FPM manual), the pool size is your concurrency limit. That size is capped by pm.max_children (documented in the FPM configuration reference). Meanwhile a worker that is waiting on a slow query is simply busy. It blocks, and that is fine, because it owns nothing anyone else needs.

The defining trait is in the name. Workers share nothing. When a request ends, its memory is reset (the PHP garbage-collection manual covers the cleanup), so a leak or a crash dies with the worker instead of poisoning the next request. This is crash isolation by construction, not by discipline.

The PHP-FPM share-nothing request lifecycleA request claims a free PHP-FPM child. The worker bootstraps fresh state, runs the handler and blocks through the DB wait, sends the response, then has its memory wiped before returning to the pool. Nothing carries between requests, so concurrency is bounded by the number of workers.

JavaScript: one thread, one event loop, non-blocking I/O#

Node runs your JavaScript on a single thread with one event loop (the runtime's own guide, "Don't Block the Event Loop", is the canonical read). Instead of parking a whole process on a slow query, Node hands the wait to libuv and moves on. Because the query is a non-blocking await, the same thread immediately picks up another request. As a result, one process interleaves thousands of requests that are all waiting at once.

That is the strength and the trap in one sentence. The thread is never idle during I/O, so throughput is high. Yet the thread is also singular, so any code that refuses to yield freezes everything. The ordering of these callbacks is worth knowing, and the event-loop timers guide walks it precisely.

One tick of the Node event loopThe single thread runs JavaScript until it hits an await on I/O. It offloads the wait to libuv, then picks up another in-flight request while the first waits. When the I/O finishes, its callback is queued, the loop drains it, and the paused request resumes. Thousands of requests interleave on one thread.

Every difference is a consequence of that one choice#

Here is the payoff of naming the fork. The differences other pages list as independent facts are all downstream of it. Walk the chain once and you can predict behaviour you have never benchmarked.

  1. The fork

    PHP scales concurrency with more processes. Node scales it with one process that never blocks on I/O.

  2. Threading falls out

    A fixed PHP worker pool versus overlapping awaits on a single Node thread. Neither is a separate design decision. Each is the fork, seen from the threading angle.

  3. Speed under load falls out

    PHP throughput is workers divided by request time. Node throughput is concurrency from non-blocking I/O. Same fork, seen from the throughput angle.

Threading and concurrency#

PHP concurrency is the worker count. Because a busy worker cannot take a second request, you raise concurrency by adding workers. Node concurrency is the number of awaits in flight. Because the thread yields on every await, one process holds many requests at once. In contrast, PHP would need one worker per simultaneous request.

Speed under load#

Speed is where the two models diverge most visibly. For I/O-bound work, both scale, but they pay differently. PHP pays in processes and RAM. Node pays in nothing extra, until the work stops being I/O-bound. Then the single thread becomes the ceiling, and we will make that failure concrete below.

Memory behavior#

Memory behaviour is a direct read of the fork. PHP state is cold and per-request, so a leak dies at request end. Node state is warm and long-lived, so a leak accumulates across requests until the process restarts. Moreover the pool model has a fixed cost even at idle, because every worker holds its own interpreter and connections.

Show data table
Approximate resident memory: a 40-worker PHP-FPM pool vs one Node process (illustrative)
Item Resident memory
PHP-FPM pool (40 workers) 1,600 MB
Node (1 process) 80 MB

The PHP pool trades roughly 1.6 GB of steady RAM for its crash isolation; the Node process holds far less because concurrency lives in one heap, not 40 interpreters.

Figure Approximate resident memory: a 40-worker PHP-FPM pool vs one Node process (illustrative) Resident memory at typical pool sizes for each runtime. Modelled, not measured.

Neither number is a verdict on its own. The pool RAM buys crash isolation and simple mental models. The single heap buys density, but it asks you to watch for leaks that never used to matter in PHP. Therefore the memory difference is a trade, not a winner.

The blocking-call failure mode#

This is the demo that teaches the whole model. Drop a synchronous 200 ms operation into a Node request handler and watch what happens to everyone else. Then drop the identical call into PHP and watch how little happens. The two snippets are the same work in two execution models.

js
// Express handler with ONE synchronous 200 ms operation in it.
app.get('/report', async (req, res) => {
  const rows = await loadRows(req.query.id)  // I/O: an await, off-thread
  const summary = summariseSync(rows)        // 200 ms of pure CPU, no await
  res.json(summary)
})

// summariseSync burns the ONE JS thread for 200 ms. While it runs, the event
// loop cannot advance, so EVERY other in-flight request is frozen until it
// returns. One slow request just became everyone's slow request. The fix is to
// offload the CPU work to a worker thread, not to add more app instances.

Because Node has one thread, the synchronous call blocks the event loop, so every concurrent client waits behind it. Because PHP is share-nothing, the same call keeps one worker busy while the rest of the pool serves normally. This single contrast is the reason "which is faster" has no answer without naming the workload. On I/O-bound work the loop wins on density. On CPU-bound work the share-nothing pool wins on isolation. The fix on Node is worker_threads, which moves the burn off the main thread.

Concurrency with real numbers#

Talk is cheap, so make the model playable. The simulator below runs the same load through both execution models and shows the throughput, the backlog, and the memory each one pays. Set the concurrency and the request time, then flip the work type and watch the Node lane change character completely.

Play the model: workers vs the loop#

Start with the worked example: 1000 connections, a 180 ms request, non-blocking I/O. First read the PHP-FPM lane, then read the Node lane. Now flip the toggle to blocking CPU. The share-nothing lane barely moves, while the event-loop lane collapses. That flip is the entire argument, made operable.

Concurrency model simulator
work per request

PHP-FPM (share-nothing)

222req/s throughput
running at once
40 of 40 workers
backlog queued
960
pool ceiling
222 req/s
pool memory
1.56 GB

Each worker blocks through its own I/O wait. Concurrency costs workers, and workers cost RAM.

Node (single-thread event loop)

5,556req/s throughput
interleaved now
1,000 in one process
stalled behind a block
0
slowest client waits
180 ms
processes
1

Every await hands the thread back, so one process interleaves all of them. Memory scales with in-flight state, not processes.

I/O-bound: both fitThis is I/O-bound work. Both models serve it. Node does it in one process because concurrency comes from overlapping awaits. PHP-FPM serves it too, but each concurrent request costs a blocked worker, so the pool trades RAM for the same result.
Worked example (illustrative): 1000 connections, a 180 ms request
ModelI/O-bound (await)CPU-bound (blocking)
PHP-FPM, 40 workers~222 req/s, ~1.6 GB, 960 queued~222 req/s, cost isolated per worker
Node, 1 processinterleaves all 1000, ~180 ms each~5.5 req/s, 999 clients stalled

The one axis: PHP-FPM ceiling is workers x 1000 / request_ms. Node interleaves non-blocking work in a single thread, but one blocking call there stalls every concurrent client until it is offloaded.

Two execution models react to one load. PHP-FPM sizing follows workers x 1000 / request_ms; the Node lane interleaves non-blocking work in one thread, then collapses when a blocking call owns it. All values are illustrative teaching numbers, not a benchmark.

I/O-bound is fine for both; CPU-bound is where the loop breaks#

The felt gap is worth stating on its own. Take one Node process at 1000 connections and a 180 ms request. Non-blocking, it interleaves all of them. Blocking, one request owns the thread and the rest queue. Here is the same process, one toggle apart.

The same Node process, 1000 connections, one toggle apartabout 1000x slower
interleaved

~5,556 req/s

Non-blocking I/O (await)

collapse

~5.5 req/s

Blocking CPU (synchronous)

One blocking call turns a healthy interleave into a near-frozen queue; the same call on PHP-FPM only ties up the single worker running it.

The same Node process, 1000 connections, one toggle apart (Illustrative req/s for a single Node process at a 180 ms request: a non-blocking await versus a synchronous CPU block that owns the only thread.)
OptionIllustrative req/s for a single Node process at a 180 ms request: a non-blocking await versus a synchronous CPU block that owns the only thread.
Non-blocking I/O (await)~5,556 req/s
Blocking CPU (synchronous)~5.5 req/s

The per-layer decision framework#

Now convert the model into judgement. The real question in PHP vs JavaScript for web development is never the whole product; it is each layer. Therefore the mistake is choosing one language for everything. Instead, choose per layer, because each layer has its own I/O and concurrency shape. The table below maps common workloads to a default, and it names the execution-model reason for each. Treat the reason as the rule; the recommendation is just its shortcut.

Match each workload layer to a runtime by its execution model, not by habit
Workload layerBetter defaultWhy, in execution-model terms
Content site / CMSPHPRequest-per-page reads suit share-nothing workers, and the CMS ecosystem is deepest here.
CRUD JSON APIEitherPure I/O-bound DB calls fit both models; decide on team skills and the rest of the stack.
Real-time / WebSocketsJavaScript (Node)One event loop holds thousands of long-lived connections; PHP-FPM would pin one worker per socket.
Streaming / long-lived responsesJavaScript (Node)Non-blocking streams ride one thread; a worker-per-stream pool exhausts its process count fast.
CPU-bound jobsPHP, or an offloaded workershare-nothing isolates the burn per worker; on Node it must go to worker_threads or a queue.
Shared full-stack logicJavaScript / TypeScriptOne language across client and server removes a whole translation and duplication layer.

The 2026 caveat: the line is blurring#

The historic split of "PHP for the backend, JavaScript for the frontend" is dissolving, and honesty demands saying so. On the PHP side, FrankenPHP, Swoole, and RoadRunner run long-lived PHP processes that hold state between requests. That erases part of the share-nothing story on purpose, in exchange for the density Node has. PHP even ships fibers now for cooperative concurrency, documented in the fibers manual.

On the JavaScript side, Node, Deno, and Bun are full runtimes, not just a frontend tool. Furthermore, shared TypeScript across client and server is now a genuine reason to standardise on one language. The spec that governs how that code executes is the ECMAScript execution-contexts chapter. If you are weighing that full-stack move, our take on the ECMAScript 2026 language features covers what modern JavaScript actually gives you.

When NOT to reach for each#

Every honest comparison needs an anti-recommendation, so here is the short version. Reach past the obvious default when the workload argues against it.

How to choose PHP vs JavaScript for web development, layer by layer#

Turn the model into a repeatable procedure. First, for each layer, ask one question. Is this work I/O-bound or CPU-bound, and how many things happen at once? Second, match the answer to an execution model. I/O-bound with high concurrency favours the event loop. CPU-bound or crash-sensitive work favours share-nothing isolation. Third, only then pick the language, because the runtime follows from the workload, not the other way around.

That is how to approach PHP vs JavaScript for web development without a religious war. You are not ranking languages. You are matching each layer to the concurrency model that serves it. For instance, a content CMS in PHP can sit happily beside a real-time notifications service in Node, and both choices are correct for the same product. The related framework debate plays out in WordPress vs Laravel in 2026, and the edge-runtime side shows up when you render a React app on Cloudflare.

If you are drawing this line for a real product and want a second opinion grounded in how the work actually runs, we do exactly this kind of architecture review. No pressure and no lock-in.

See how we approach software engineering

Kalpesh Patel

Software Engineer, Atyantik Technologies

Kalpesh Patel is a Software Engineer at Atyantik Technologies, a software product studio building web platforms and integrated systems since 2015. His expertise is WordPress and frontend design.

More from Kalpesh PatelHow we buildTalk to the team

Keep reading