← Tous les cours / PHP — Concurrence & Runtimes Cours
00 — VUE D'ENSEMBLE

Comment PHP
traite plusieurs requêtes

Une question, un fil rouge : quand mille visiteurs arrivent en même temps, qui fait quoi, et où va la mémoire ?

PHP, tout seul, ne sait faire qu'une chose à la fois : exécuter un script du début à la fin, dans un seul fil. Toute la magie de la « simultanéité » se joue donc autour de PHP — dans le système d'exploitation (processus, threads), dans le serveur web (Nginx, Apache, Caddy) et dans le runtime qui héberge PHP (FPM, FrankenPHP…).

Ce site décompose la chaîne, couche par couche. Les briques de base d'abord (processus, threads, concurrence), puis comment elles s'assemblent dans les vraies architectures PHP.

Le chemin d'une requête

Client
Navigateur
HTTP
Serveur web
Nginx / Apache
FastCGI
Runtime
PHP-FPM
exécute
Code
PHP / Symfony

Par où commencer

01 — LES BRIQUES DE BASE

Processus & Threads

Avant PHP, avant tout serveur, il faut comprendre les deux unités que le système sait faire tourner.

Le processus

Un processus, c'est un programme en cours d'exécution, avec sa propre mémoire isolée, ses propres ressources et son propre identifiant (PID). C'est une bulle étanche : ce qui se passe dedans n'affecte pas les autres.

Quand tu tapes php index.php, le système crée un processus. Tu lances la commande deux fois → deux processus totalement séparés, chacun avec ses variables. L'un peut planter sans toucher l'autre.

PID 1234
Processus A
mémoire ████
isolés
PID 1235
Processus B
mémoire ████

Au fait, c'est quoi l'IPC ?

IPC = Inter-Process Communication, la « communication entre processus ». Comme chaque processus vit dans sa bulle mémoire étanche, il ne peut pas simplement lire les variables d'un autre. Pour échanger des données, il doit passer par un canal explicite que le système d'exploitation met à disposition : c'est ça, l'IPC.

L'image : deux personnes dans deux pièces fermées et insonorisées (les deux processus). Elles ne s'entendent pas. Pour communiquer, il leur faut un dispositif convenu — glisser un mot sous la porte, un interphone, un tube. Ces « dispositifs », côté système, ce sont les mécanismes d'IPC :

🔌Tu en as déjà croisé un sans le nommer : le socket Unix entre Nginx et PHP-FPM (page FastCGI) est précisément de l'IPC. Deux processus séparés — le serveur web et l'interpréteur — qui se parlent à travers un canal dédié. C'est pour cela qu'on dit que la communication entre processus est « compliquée » comparée aux threads : ces derniers, partageant la même mémoire, n'ont besoin d'aucun de ces mécanismes pour échanger.

Le thread

Un thread (fil d'exécution) vit à l'intérieur d'un processus. Plusieurs threads d'un même processus partagent la même mémoire et peuvent s'exécuter en parallèle.

L'image du restaurant : le processus est le restaurant entier (avec sa cuisine = la mémoire) ; les threads sont les serveurs qui travaillent en même temps dans cette même cuisine. Créer un serveur de plus coûte bien moins cher que d'ouvrir un nouveau restaurant.

Processus vs thread, côte à côte

CritèreProcessusThread
MémoireIsolée, à soiPartagée avec les autres threads
Coût de créationélevéfaible
Isolation / sûretétotaleaucune (race conditions)
CommunicationComplexe (IPC)Simple (mémoire commune)
Un crash……reste confiné…peut emporter tout le processus
🔑Retenir : isolation et sûreté d'un côté (processus), légèreté et partage de l'autre (threads). Tout le reste de ce site découle de ce compromis.
02 — METTRE PLUSIEURS UNITÉS AU TRAVAIL

Multiprocessus vs Multithreading

Deux façons de traiter plusieurs choses à la fois — l'une sûre et gourmande, l'autre légère et délicate.

Multiprocessus

Lancer plusieurs processus indépendants, chacun dans sa bulle mémoire, pour traiter plusieurs tâches en même temps.

C'est exactement le modèle de PHP-FPM : un pool d'interpréteurs PHP, chacun dans son processus. Une requête = un processus qui s'en occupe entièrement. Comme la mémoire est isolée, si un script plante (segfault, fuite mémoire), il n'emporte que sa requête.

Multiprocessus (FPM, Apache prefork)
+Isolation totale : un crash ne tue pas les voisins
+Simple à raisonner et à déboguer ; pas de partage = pas de race condition
Mémoire : chaque processus recharge tout l'interpréteur et le code
Plafond de processus vite atteint (RAM)

Multithreading

Plusieurs threads dans un même processus, partageant la mémoire. Plus léger : on ne duplique pas l'interpréteur, on ajoute juste des fils.

PHP n'est pas multi-threadé par défaut, et c'est un choix volontaire de sûreté. Chaque script s'exécute dans un seul fil, du haut vers le bas. Pour faire du vrai multi-threading il faut une version « thread-safe » de PHP (ZTS) et un runtime qui l'exploite : c'est ce que font Swoole et FrankenPHP.

Le piège : la race condition

Le partage de mémoire est puissant mais dangereux : si deux threads modifient la même variable « en même temps », les écritures peuvent s'écraser. Lance la démo : deux threads font chacun +1 cinq fois sur un compteur partagé. Sans verrou, le total final est faux.

compteur = 0
Attendu : 10. Choisis un mode et lance.
La démo est illustrative (l'entrelacement est scénarisé), mais le phénomène est réel : sans synchronisation (mutex, opérations atomiques), des incréments se perdent. C'est la raison pour laquelle PHP a longtemps évité les threads.
Idée maîtresse : le multithreading ne rend pas une requête plus rapide. Il permet de traiter plus de requêtes à la fois, avec moins de mémoire que le multiprocessus — au prix de la prudence sur l'état partagé.
03 — « EN MÊME TEMPS », VRAIMENT ?

Concurrence vs Parallélisme

Le mot « simultané » cache deux réalités très différentes. Le simulateur les met côte à côte.

La concurrence : plusieurs tâches progressent sur une même période, mais pas forcément au même instant physique. Un seul cœur alterne très vite entre elles (context switching). Illusion de simultanéité.
Le parallélisme : plusieurs tâches s'exécutent physiquement au même instant, sur plusieurs cœurs CPU. Vraie simultanéité.

Une formule utile : la concurrence parle de structure (gérer plusieurs choses), le parallélisme parle d'exécution (faire plusieurs choses à la fois). On peut avoir de la concurrence sans parallélisme (1 cœur), et il faut plusieurs cœurs pour du vrai parallélisme.

4 tâches identiques à exécuter de chaque côté

🧵 CONCURRENCE — 1 cœurt = 0

Le cœur jongle entre les tâches. Toutes avancent… mais chacune son tour.

⚡ PARALLÉLISME — 4 cœurst = 0

Chaque cœur prend une tâche. Tout avance vraiment en même temps.
Tâche 1 Tâche 2 Tâche 3 Tâche 4

Note l'horloge : avec un seul cœur, le temps total pour finir les 4 tâches est ~4× plus long. Le parallélisme ne « divise » le temps que s'il y a assez de cœurs et que les tâches sont indépendantes.

Et le chargement d'une page web ?

Quand le navigateur charge une page, il réclame plusieurs ressources (CSS, JS, images) « en parallèle ». Mais attention : c'est géré par le navigateur et le serveur HTTP, pas par PHP.

PHP, lui, ne voit que des requêtes individuelles qui lui arrivent une par une. La concurrence côté serveur PHP, c'est l'affaire de FPM ou du runtime — les pages suivantes.

04 — CE QU'EST PHP, EXACTEMENT

La nature de PHP

PHP est un interpréteur, pas un serveur. Cette phrase explique presque toute la suite.

PHP, seul, ne sait pas recevoir une requête HTTP. Il lui faut un serveur web devant pour accepter les connexions réseau, et un runtime / SAPI qui décide comment l'appeler et lui passer la requête.

Les SAPI : les visages de PHP

Un SAPI (Server API) est l'interface par laquelle PHP est invoqué. Le même langage, plusieurs portes d'entrée :

SAPIContexteExemple
cliLigne de commandephp script.php
fpm-fcgiPHP-FPM via FastCGINginx → PHP-FPM
apache2handlerModule dans ApacheApache + mod_php
embedEmbarqué dans un autre programmeFrankenPHP (dans Caddy/Go)

Le cycle de vie « classique » d'une requête

# Au démarrage du worker FPM — UNE SEULE FOIS :
0. PHP lit php.ini, initialise le moteur et les extensions   # MINIT

# Puis, à CHAQUE requête (le worker reste vivant) :
1. Navigateur  ─HTTP→  Nginx / Apache      # reçoit la connexion
2. Serveur web ─FastCGI→ PHP-FPM             # transmet la requête
3. PHP charge le script (autoloader, bootstrap Symfony…)
4. PHP exécute et produit le HTML / JSON
5. Fin de requête : variables et état jetés, mémoire libérée   # le worker NE meurt PAS
6. Réponse ─HTTP→ Navigateur

Le point crucial : à chaque requête, l'application (autoloader, conteneur, configs) est rebootée puis détruite — mais le worker, lui, ne meurt pas et ne relit ni php.ini ni les extensions (ça, c'est une fois au démarrage). Pour un gros framework (Symfony, Laravel), reconstruire le conteneur, l'autoloader et les configs à chaque hit a un coût réel.

💡OPcache atténue ce coût en gardant en mémoire le bytecode compilé des fichiers PHP (plus besoin de re-parser le code source à chaque fois). Mais l'état de l'application (objets, conteneur) est quand même reconstruit. C'est précisément ce que le mode worker de FrankenPHP/Swoole supprime — voir la page Runtimes.
05 — LE TUYAU & LE PROTOCOLE

FastCGI

Comment le serveur web parle à PHP. Pas une « mise en binaire » du HTTP : un protocole à part entière.

D'où ça vient : le problème de CGI

Au début du web dynamique, il y avait CGI. Le principe : à chaque requête, le serveur démarre un nouveau processus, lui fait exécuter le script, récupère le résultat, puis tue le processus. Démarrer un interpréteur à chaque visiteur, des milliers de fois par seconde, est un gâchis énorme.

FastCGI (1996) corrige ça avec une idée simple : garder les processus vivants et les réutiliser. On démarre un petit groupe d'interpréteurs une fois, et chaque requête est confiée à un processus déjà « chaud ».

🐌 CGI — un processus par requêtedémarrages : 0

Chaque requête naît un processus puis le tue. Le compteur explose.

⚡ FastCGI — pool réutilisédémarrages : 4

4 processus démarrés une fois. Ils s'allument, traitent, se rendorment.

Le tuyau, et ce qui voyage dedans

Le socket de domaine Unix (ex. /run/php/php8.2-fpm.sock) est juste un fichier spécial servant de point de rendez-vous entre deux programmes — exactement comme docker.sock. Ce qui change, c'est le protocole qui circule dedans.

Serveur web
Nginx
unix:/run/php/php-fpm.sock
REQ
HTML
Interpréteur
PHP-FPM
→ Aller : records FCGI_PARAMS + FCGI_STDIN
SCRIPT_FILENAME/var/www/app/public/index.php
REQUEST_METHODPOST
REQUEST_URI/checkout?cart=42
QUERY_STRINGcart=42
HTTP_HOSTmonsite.fr
HTTP_COOKIEPHPSESSID=8f3a…
STDIN (corps){"qty":2,"coupon":"NOEL"}
← Retour : record FCGI_STDOUT
Status200 OK
Content-Typetext/html; charset=utf-8
Body<!DOCTYPE html>…la page générée…</html>

La nuance qui fait tout comprendre

On dit souvent « le serveur convertit le HTTP en binaire ». C'est trompeur : le HTTP circule déjà sous forme d'octets sur le réseau. Le serveur ne prend pas le texte HTTP brut pour le pousser tel quel dans le tuyau. Il :

  1. parse la requête HTTP entrante,
  2. en extrait les informations utiles (méthode, URL, en-têtes, corps),
  3. reconstruit des paramètres FastCGI structurés (REQUEST_METHOD, QUERY_STRING, SCRIPT_FILENAME…),
  4. les sérialise dans des messages binaires FastCGI (les « records »).

PHP-FPM décode ensuite ces records et reconstitue les variables que tu connais : $_SERVER, $_GET, $_POST… C'est pourquoi le mieux est de voir FastCGI comme un protocole RPC spécialisé : le serveur web ne « transmet » pas une requête, il appelle PHP-FPM en lui disant « exécute ce script avec ces paramètres-là ».

# En texte, côté HTTP, Nginx reçoit ceci :
GET /index.php?id=123 HTTP/1.1
Host: example.com
User-Agent: Chrome

# Il ne le recopie PAS. Il le transforme en paramètres FCGI :
REQUEST_METHOD = "GET"
SCRIPT_FILENAME = "/var/www/index.php"
QUERY_STRING = "id=123"
HTTP_HOST = "example.com"
# …puis sérialise tout ça en records binaires dans le socket.
Navigateur
HTTP
Nginx / Apache / Caddy / Traefik
FastCGI (RPC)
PHP-FPM
exécute
Interpréteur PHP
🌍FastCGI n'a rien de spécifique à PHP. Historiquement il servait à parler à des applications dans d'autres langages, et plusieurs serveurs le parlent (Nginx, Apache, Caddy, Traefik, lighttpd…). Aujourd'hui, son usage de loin le plus répandu reste PHP-FPM.
# Côté Nginx, brancher le tuyau tient en deux lignes :
location ~ \.php$ {
    include fastcgi_params;                       # mappe HTTP → variables FCGI
    fastcgi_pass unix:/run/php/php8.2-fpm.sock;   # le tuyau
}
06 — LE GESTIONNAIRE D'OUVRIERS

PHP-FPM : le pool

FPM = FastCGI Process Manager. Il maintient un pool de processus PHP prêts à travailler. Joue avec.

Chaque worker traite une seule requête à la fois, du début à la fin. Envoie des requêtes, change le mode, et regarde la file d'attente se remplir puis se vider.

💡Clique sur un worker occupé (ambre) pour terminer sa requête à la main. Si la file déborde, tu verras le 502.
0
occupés
0
en file
0
servies
0
erreurs 502
FILE D'ATTENTE (backlog)0 / 8
Pool prêt. Envoie une requête.

Les trois modes pm

Le mode décide quand les workers existent (dans www.conf). Change-le dans le simulateur : en ondemand les slots restent vides tant qu'aucune requête n'arrive ; en static tous les workers sont là d'emblée.

ModeComportementPour quoi
staticTous les workers (pm.max_children) démarrent et restent en vie, occupés ou non.Trafic élevé et constant. Prévisible, mais réserve la RAM en permanence.
dynamicDémarre avec pm.start_servers, en crée d'autres à la demande jusqu'à max_children, détruit les inactifs en trop.Le défaut recommandé. Bon compromis (Symfony, Laravel…).
ondemandAucun worker au repos. Chaque requête en crée un, détruit après pm.process_idle_timeout.Beaucoup de petits sites sur un serveur. Zéro gâchis RAM, latence au 1er hit.

Quand tout sature → 502

Workers tous occupés → les requêtes entrent dans la file d'attente. Si la file est pleine (ou qu'une requête dépasse request_terminate_timeout), FPM renvoie une 502 Bad Gateway au serveur web.

D'où l'enjeu de calibrer pm.max_children : trop peu → des 502 sous charge ; trop → RAM épuisée (chaque worker ≈ 30–60 Mo). Règle de pouce : max_children ≈ RAM dispo ÷ RAM par worker.

07 — LES VRAIES ARCHITECTURES

Runtimes & architectures

Quatre montages classiques. Pour chacun : qui parle HTTP, comment la concurrence est gérée, et où va la mémoire.

Chemin d'une requête

Occupation mémoire (schématique)

⚙️Petit point sur la concurrence du serveur web lui-même : Apache prefork ouvre un processus par connexion (simple, gourmand). Nginx est événementiel : quelques processus gèrent des milliers de connexions de façon asynchrone (très léger). C'est pourquoi le couple Nginx + FPM domine pour servir beaucoup de trafic.
08 — LE MODÈLE THREADÉ

FrankenPHP : pool de threads

FPM met en pool des processus. FrankenPHP met en pool des threads, dans un seul processus. Ça change tout — voici pourquoi.

FrankenPHP fait bien du scaling dynamique « façon FPM », mais l'unité n'est pas la même. Le programme frankenphp est un binaire écrit en Go (bâti sur le serveur Caddy) dans lequel l'interpréteur PHP est embarqué (via cgo). Ce programme est multithreadé, et ce sont ses threads qui exécutent ton code PHP.

La correction du modèle mental

Ce n'est pas « un seul worker avec plusieurs threads ». C'est : plusieurs workers, et chaque worker EST un thread, tous logés dans un seul processus (le binaire Go/Caddy). Chaque thread traite une seule requête à la fois — exactement comme un worker FPM traite une requête à la fois. La seule différence, c'est l'unité : thread au lieu de processus.

Thread = processus ? Non, vraie différence

Ce n'est pas qu'une question de vocabulaire : ce sont deux primitives différentes du système d'exploitation.

Processus (FPM)Thread (FrankenPHP)
MémoireEspace isolé, à soiPartagé avec les autres threads du processus
Si l'un planteLes autres surviventPeut corrompre/emporter le processus
Coûtlourd (RAM, context switch)léger
Copies de l'interpréteurN copies indépendantes1 seule, partagée

Visualise-le : 10 processus isolés vs 10 threads partagés

Bouge le curseur : à gauche FPM duplique tout l'interpréteur par processus ; à droite FrankenPHP charge l'app une seule fois et n'ajoute que des threads légers.

🧱 PHP-FPM — pool de processus

Chaque requête = un processus isolé, avec sa propre copie de tout.

🧵 FrankenPHP — pool de threads

Un seul processus. L'app est résidente et partagée ; on n'ajoute que des threads.
📏Chiffres illustratifs (l'empreinte réelle dépend de ton app et de memory_limit). Mais la tendance est exacte : les threads partagent l'OPcache et le code compilé au lieu d'en avoir N copies — d'où une RAM bien plus basse.

Le scaling : num_threads & max_threads

L'équivalent du pm = dynamic de FPM se règle dans le bloc frankenphp. Le scaling est piloté par la profondeur de la file : quand tous les threads sont occupés, les requêtes sont mises en file ; si la file dépasse un seuil, FrankenPHP démarre des threads supplémentaires jusqu'à max_threads.

ConceptPHP-FPMFrankenPHP
Unitéprocessusthread OS
Démarrage initialpm.start_serversnum_threads / num (worker)
Plafond (autoscale)pm.max_childrenmax_threads
Mode fixepm = staticmax_threads = num_threads
Mode élastiquepm = dynamicmax_threads > num_threads
File pleine →502 immédiatattente, puis rejet si max_wait_time dépassé
# Caddyfile — pool élastique en mode worker
frankenphp {
    num_threads 4      # threads PHP au démarrage (≈ pm.start_servers)
    max_threads 20     # plafond, montée auto sous charge (≈ pm.max_children)
}

example.com {
    php_server {
        root /app/public
        worker {
            file index.php
            num 4       # instances du worker tenues chaudes en mémoire
        }
    }
}
⚙️max_threads auto estime le plafond depuis le memory_limit de ton php.ini (sinon il retombe sur 2× num_threads). La doc prévient qu'auto peut fortement sous-estimer : règle de stabilité recommandée → num_threads × memory_limit < RAM disponible, et fais des tests de charge.

Deux modes, et « thread = worker » seulement dans l'un

ModeCe que font les threadsAnalogie FPM
classic (défaut)Traitent les requêtes mais réinitialisent l'état PHP après chacune. Drop-in remplaçant de FPM/mod_php.FPM, mais en threads
workerChaque thread charge le script une seule fois, garde l'app bootstrapée en mémoire, et boucle sur les requêtes. , thread = worker persistant.Worker FPM, mais persistant et chaud

Donc la formule juste : FrankenPHP est un programme Go multithreadé ; en mode worker, chacun de ces threads est un worker qui garde ton app en mémoire. En mode classique, on parle juste de « threads » (pas de notion de processus qui reste chaud entre requêtes).

Les contreparties du partage

Comme tout est dans un seul processus à mémoire partagée, FrankenPHP doit gérer ce que l'isolation de FPM offrait gratuitement :

🔁Le détail qui boucle la boucle : l'unité d'échelle de FrankenPHP est le thread, pas le processus. Son « scaling dynamique » fait donc monter/descendre le nombre de threads (max_threads), là où FPM fait varier le nombre de processus (pm.max_children). Même idée de pool élastique, deux primitives différentes.
09 — TOUT RASSEMBLER

Synthèse

Le tableau de bord, et le réflexe à garder pour chaque concept.

Comparatif des architectures

ArchitectureServeur HTTPConcurrence PHPMémoireBootstrapUsage
Apache + mod_phpApache (prefork)Multiprocessusélevéechaque requêtelegacy
Apache + PHP-FPMApache (event)Multiprocessusmoyennechaque requête*migration
Nginx + PHP-FPMNginx (async)Multiprocessusmoyennechaque requête*standard
FrankenPHPCaddy (intégré)Threads / goroutinesfaibleune foismoderne
Swoole / RoadRunnerintégréCoroutines / workersfaibleune foishaute perf

* Avec OPcache, le bytecode est mis en cache : le coût du bootstrap chute, mais l'état applicatif est tout de même reconstruit à chaque requête.

Une phrase par concept

ProcessusProgramme en exécution, mémoire isolée.
ThreadFil dans un processus, mémoire partagée.
MultiprocessusPlusieurs processus → plus de requêtes en parallèle, mémoire isolée mais lourde.
MultithreadingPlusieurs threads → plus léger, mais attention à l'état partagé.
ConcurrencePlusieurs tâches progressent (pas forcément au même instant).
ParallélismePlusieurs tâches au même instant (multi-cœurs).
FastCGIProtocole RPC entre serveur web et interpréteur ; réutilise les processus.
RuntimeL'environnement qui exécute PHP et lui amène les requêtes.

Guide de choix express

🧭Le fil rouge : on échange toujours isolation/simplicité contre légèreté/vitesse. FPM penche vers la sûreté ; FrankenPHP/Swoole vers la performance, au prix d'une vigilance sur l'état partagé entre requêtes.