JavaScript’s event loop vs. PHP’s multi-process model

Last updated: February 5th 2025

Introduction

Imagine two restaurants competing on the same busy street. One uses a single waiter with magic notepad (JavaScript’s event loop), and the other hires 100 waiters with one hand each (PHP’s multi-process model). Their strategies reveal why JavaScript and PHP handle tasks so differently, even though both are “single-threaded.”

Let's break it down and understand how JavaScript (Node.js) and PHP handle tasks internally.

JavaScript’s Eventloop

Event Loop is just a loop that runs forever

while (true) {
   // do something
}

what happend in Node ?

Nodejs uses OS-specific APIs (e.g., epoll on Linux, kqueue on macOS, IOCP on Windows) to monitor I/O events. When a request (e.g., HTTP, TCP) arrives, the OS notifies libuv, which then enqueues the associated callback into the event loop.For blocking operations (e.g., file I/O), libuv offloads work to a thread pool and inserts the result into the event loop once ready.

Event Loop Phases

The event loop constantly checks for tasks in this order:

  • Checks Timers and excutes callbacks from setTimeout() or setInterval().
  • Checks Pending Callbacks and Runs I/O-related callbacks (e.g., network errors).
  • Idle/Prepare: these are Internal system tasks (ignored by developers, you just need to know that they exist).

How Async Works

  • When you call fetch() or readFile(), Node.js sends the task to the OS kernel (via libuv).
  • The main thread keeps running other code as usual.
  • Once the OS finishes the task, its callback is added to the queue.
  • The event loop picks it up in the next iteration.

Example

console.log("Start");
setTimeout(() => console.log("Timer"), 0);
fetch("https://api.com").then(() => console.log("API Done"));
console.log("End");

output:

Start  
End  
Timer  
API Done
  • The setTimeout and fetch tasks are offloaded to the OS.
  • The main thread runs console.log("End") before handling their callbacks.

PHP Under the Hood

(Multi-Process Model)

One Process Per Request

  • Each HTTP request starts a new PHP process (or thread).
  • Example: 100 users = 100 separate processes.

Blocking Execution

If a task (e.g., database query) takes time, the process stops until it finishes.

Example:

echo "Start";
$data = file_get_contents("large_file.txt"); // Blocks here
echo "End";

The process waits for file_get_contents() to finish before printing “End”.

PHP-FPM (FastCGI Process Manager)

  • Uses a pool of worker processes to handle requests.
  • Workers are reused, but each request still runs in isolation.
  • Heavy traffic? The server spawns more workers, consuming RAM/CPU.

No Shared Memory

  • Variables, sessions, and data are not shared between requests.
  • Each process starts fresh.

Key Differences in Action

Scenario JavaScript (Node.js) PHP
100 Users Request Data 1 thread handles all via event loop. 100 processes (RAM overload).
Database Query Keeps processing other tasks while waiting. Process blocks until query ends.
Crash Whole server crashes if the main thread dies. Only one process crashes.

Why JavaScript Scales Better

  • Low Overhead: One thread + OS helpers (libuv) handle thousands of tasks.
  • Non-Blocking: No wasted time waiting for I/O (files, networks, databases).

Why PHP Simpler for Basic Sites

  • No Async Complexity: Code runs top-to-bottom.
  • Isolation: Crashes or bugs affect only one request.

Conclusion: When to Use Which

  • JavaScript: Apps needing real-time updates, high concurrency (APIs, chats).
  • PHP: Sites with simple logic and low traffic (blogs, CMS).

This article was written by Ahmad AdelAhmad is a freelance writer and also a backend developer.