←Tous les articles
Lun. 28 septembre 2026·9 min de lecture

🐘 Qbix Server : un serveur web Ă©crit en pur PHP

Schéma Qbix Server : le process parent charge l'application, pcntl_fork() en copy-on-write, 5 000 workers à environ 120 Ko

📚 Introduction

Quand on veut sortir de PHP-FPM, le rĂ©flexe en 2026 c’est FrankenPHP, Ă©ventuellement Swoole ou RoadRunner via Octane. Tous partagent une caractĂ©ristique : le serveur n’est pas Ă©crit en PHP. Go et Caddy pour l’un, une extension C pour l’autre.

Qbix Server prend le chemin inverse. C’est un serveur HTTP Ă©crit en PHP pur, lancĂ© par un simple php qbixserver.php, qui prĂ©tend remplacer nginx, php-fpm, supervisor et une partie de Redis ou Node.js par un seul process. Le projet est sous licence MIT, maintenu par Qbix sur GitHub, et vient de sortir en v2.0.0.

L’idĂ©e paraĂźt absurde au premier abord. Un serveur web en PHP, plus rapide que FPM ? En regardant de prĂšs, le mĂ©canisme tient debout, et il repose sur une fonctionnalitĂ© de l’OS que PHP-FPM exploite mal : le copy-on-write.

🔍 Comment un serveur en PHP peut-il tenir la charge ?

RĂ©ponse courte : en laissant le noyau faire le travail. Qbix charge le framework une fois dans un process parent, puis appelle pcntl_fork(). Chaque worker hĂ©rite des pages mĂ©moire du parent, marquĂ©es copy-on-write : il ne paie que ce qu’il modifie. Selon le README, un worker coĂ»te environ 120 Ko au lieu des 30 Ă  60 Mo d’un worker FPM.

C’est là que se joue l’argument. Sur une app qui passe son temps à attendre la base ou une API externe, le facteur limitant de FPM n’est pas le CPU mais le nombre de workers qui tiennent en RAM. Swoole rùgle ce problùme avec des coroutines, à condition d’utiliser ses clients I/O. FrankenPHP et RoadRunner gardent un modùle à nombre de workers fixe. Qbix garde du code PHP bloquant tout à fait classique et lance simplement beaucoup plus de workers : le projet annonce environ 5 000 workers concurrents sur 1 Go de RAM, contre une vingtaine pour FPM.

đŸ§± Deux modes d’exĂ©cution

Par dĂ©faut, Qbix tourne en workers persistants (le projet parle aussi de mode « octane »). Le worker survit entre les requĂȘtes, comme avec FrankenPHP en worker mode. Le problĂšme classique de ce modĂšle, ce sont les fuites d’état : une propriĂ©tĂ© statique remplie par la requĂȘte A se retrouve dans la requĂȘte B.

Qbix attaque le problĂšme sur deux fronts. D’abord, il réécrit Ă  la volĂ©e, au chargement des fichiers, 27 fonctions natives qui n’ont pas de sens en SAPI CLI : header(), setcookie(), session_start(), ini_set(), set_error_handler(), spl_autoload_register(), putenv()
 Elles sont suivies par requĂȘte et remises Ă  leur valeur de boot. Ensuite, entre deux requĂȘtes, un snapshot basĂ© sur Reflection restaure toutes les propriĂ©tĂ©s statiques (environ 0,03 ms), repeuple les superglobales et vide $_SESSION et les buffers de sortie.

Quand ça ne suffit pas, il reste le fork-per-request : chaque requĂȘte s’exĂ©cute dans un process forkĂ© qui meurt ensuite. Aucune fuite possible, puisque le process disparaĂźt. C’est plus lent, mais on conserve le bĂ©nĂ©fice du copy-on-write. Et on peut mĂ©langer les deux, en routant seulement certains chemins vers le mode fork :

"fork": { "patterns": ["legacy/.*", "unsafe-extension.php"] }

🚀 DĂ©marrer avec Laravel ou WordPress

Pas de Composer à installer cÎté app. On clone le dépÎt et on pointe le serveur sur son projet :

git clone https://github.com/Qbix/webserver
cd my-laravel-app
php /path/to/webserver/qbixserver.php --root=public --preset=laravel --port=8080

Pour WordPress, mĂȘme principe avec --root=. --preset=wordpress. Les presets rĂšglent les limites d’upload, la mĂ©moire et le GC des sessions (64M d’upload pour WordPress, calĂ© sur ce qu’affiche son uploader). La doc liste aussi Symfony, Drupal, Joomla et Magento comme compatibles sans modification.

Le composer.json du projet demande PHP 8.1 et ext-sockets. pcntl est prĂ©sentĂ© comme optionnel, mais sans lui on perd tout l’intĂ©rĂȘt du copy-on-write : sous Windows, Qbix se replie d’ailleurs sur des sous-process php-cgi. Des binaires autonomes (PHP embarquĂ©) sont publiĂ©s pour Linux, macOS et Windows.

🐳 Un Dockerfile pour Laravel

Qbix ne publie pas d’image Docker officielle. Voici celle que j’ai montĂ©e et testĂ©e sur un Laravel 13 fraĂźchement installĂ©, avec Qbix v2.0.0 et PHP 8.4. Elle reprend le principe du build multi-stage pour Laravel : Composer dans une premiĂšre Ă©tape, un runtime lĂ©ger dans la seconde.

# Étape 1 : dĂ©pendances Composer
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --optimize --no-dev --no-scripts
 
# Étape 2 : runtime Qbix
FROM php:8.4-cli-bookworm
 
RUN apt-get update && apt-get install -y --no-install-recommends git \
	&& docker-php-ext-install pcntl sockets pdo_mysql \
	&& rm -rf /var/lib/apt/lists/*
 
RUN git clone --depth 1 --branch v2.0.0 https://github.com/Qbix/webserver /opt/qbix
 
# Qbix v2.0.0 émet des notices Deprecated sous PHP 8.4 : on les garde hors des réponses
RUN printf 'display_errors=stderr\nerror_reporting=E_ALL & ~E_DEPRECATED\n' \
	> /usr/local/etc/php/conf.d/qbix.ini
 
WORKDIR /app
COPY --from=vendor --chown=www-data:www-data /app /app
# Qbix écrit ses logs dans local/ et ses clés mesh dans keys/, relatifs au dossier courant
RUN mkdir -p local/logs keys && chown -R www-data:www-data local keys
USER www-data
 
EXPOSE 8080
HEALTHCHECK CMD curl -fs http://127.0.0.1:8080/up || exit 1
CMD ["php", "/opt/qbix/qbixserver.php", "--root=public", "--preset=laravel", "--port=8080", "--config=/app/qbix.json"]

Et le fichier qbix.json Ă  la racine du projet :

{
	"Q": {
		"webserver": {
			"forkPerRequest": true
		},
		"dashboard": false
	}
}

Trois choix méritent une explication, parce que je les ai tous appris en cassant le conteneur.

Le forkPerRequest: true d’abord. En mode persistant, la premiĂšre requĂȘte passe, puis toutes les suivantes renvoient une 500 : Call to a member function handleRequest() on true. Le public/index.php de Laravel charge l’application avec require_once, et dans un worker qui a dĂ©jĂ  exĂ©cutĂ© ce fichier, require_once renvoie true au lieu de l’objet Application. En mode fork, chaque requĂȘte repart d’un process neuf et tout fonctionne. La promesse « Laravel sans modification » tient donc, mais pas dans le mode par dĂ©faut.

Ensuite "dashboard": false. Sans cette ligne, /Q/dashboard et /Q/health sont accessibles Ă  n’importe qui sur le port exposĂ©. Une fois le dashboard dĂ©sactivĂ©, /Q/health rĂ©pond 404, d’oĂč le healthcheck branchĂ© sur la route /up de Laravel. Dans mes tests, la page HTML de /Q/dashboard reste quand mĂȘme servie (sans ses donnĂ©es temps rĂ©el) : derriĂšre un reverse proxy, je bloquerais le prĂ©fixe /Q/ par prĂ©caution.

Enfin le fichier .ini. Sous PHP 8.4, Qbix v2.0.0 dĂ©clenche des notices « Creation of dynamic property » qui, avec la config par dĂ©faut de l’image, atterrissent directement dans le corps des rĂ©ponses HTTP. Les renvoyer sur stderr suffit.

🧰 Ce qu’il y a dans la boüte

C’est la partie qui m’a le plus surpris, parce qu’on sort largement du rîle de serveur HTTP :

  • HTTP, WebSocket et SSE sur le mĂȘme port, avec des rooms Ă  la Socket.IO ;
  • les headers X-Accel-Redirect (fichiers protĂ©gĂ©s servis par le serveur aprĂšs contrĂŽle d’accĂšs, comme avec nginx), X-Cache-Tree pour le cache de fragments, ETag et 304 ;
  • redimensionnement d’images Ă  la volĂ©e (?w=300) avec nĂ©gociation AVIF/WebP ;
  • TLS, cron et access log intĂ©grĂ©s ;
  • HTTP/2 via AMPHP en dĂ©pendance optionnelle.

La v2 ajoute une couche mesh (P2P chiffrĂ© sur Bluetooth, Wi-Fi ou TCP, synchronisation par filtres de Bloom et prolly trees) et un runtime iOS/Android. On parle lĂ  d’un projet aux ambitions trĂšs larges, bien au-delĂ  du simple remplacement de FPM.

🎯 Qbix vs FrankenPHP et Swoole

Sur le fonctionnement, les trois outils ne jouent pas du tout la mĂȘme partition :

FrankenPHPSwooleQbix Server
Écrit enGo + C (embarque PHP)Extension C pour PHPPHP pur
InstallationBinaire Go ou image Dockerpecl install swoole (compile du C)php qbixserver.php, rien Ă  installer
ModĂšleWorkers persistantsCoroutines dans des workers persistantsWorkers persistants ou fork par requĂȘte (COW)
Fuites d’étatPossibles, statiques Ă  auditerPossibles, globales Ă  surveillerReset par snapshot, ou impossibles en fork
Code existantFonctionne dans la grande majoritéI/O bloquantes à réécrireFonctionne tel quel (27 fonctions réécrites)
HTTP/2Oui (Caddy), HTTP/3 aussiOuiVia AMPHP (optionnel)
WebSocketVia MercureIntégréIntégré, avec rooms
X-Accel-RedirectNonÀ coder soi-mĂȘmeIntĂ©grĂ©

Et les assets statiques ? Qbix les sert lui-mĂȘme, sans nginx devant : ETag et rĂ©ponses 304, compression, et redimensionnement d’images Ă  la volĂ©e (?w=300) avec conversion automatique en AVIF ou WebP selon le navigateur. Pour un site qui servait ses images via nginx plus un service de resize sĂ©parĂ©, c’est une brique de moins.

Le projet publie aussi des chiffres de débit, que je reprends tels quels :

Débit annoncé par QbixFrankenPHPSwooleQbix Server
PHP, calcul pur (CPU-bound)350 req/s~400 req/s2 294 req/s (100 workers persistants)
PHP, 200 Mo de RAM, I/O de 50 ms78 req/s~300 req/s151 (fork) / 1 060 req/s (persistant)
PHP, 200 Mo de RAM, I/O de 200 ms20 req/s~200 Ă  500 req/s94 (fork) / 488 req/s (persistant)
Fichiers statiques10 038 req/s26 974 req/s18 311 req/s

À lire avec rĂ©serve : ce sont des mesures maison, et un FrankenPHP Ă  350 req/s en CPU-bound laisse penser Ă  une config peu reprĂ©sentative (mode classique plutĂŽt que worker mode ?). Notez d’ailleurs que sur les statiques, Swoole reste devant. À reproduire sur sa propre app avant d’y croire.

Ce qui me paraĂźt solide, c’est l’argument structurel. En mĂ©moire, un worker forkĂ© en COW coĂ»te rĂ©ellement trĂšs peu, et sur de l’I/O bloquant, plus de workers donne mĂ©caniquement plus de dĂ©bit. CĂŽtĂ© compatibilitĂ©, Qbix n’exige aucune réécriture façon Swoole. En revanche, il n’offre ni HTTP/3 ni l’écosystĂšme Caddy, et la communautĂ© reste confidentielle (autour de 160 Ă©toiles sur GitHub).

⚠ Quelques prĂ©cautions

Tout n’est pas remis Ă  zĂ©ro entre deux requĂȘtes. Le worker reste vivant, donc certaines choses créées pendant une requĂȘte sont encore lĂ  Ă  la suivante. Qbix nettoie les variables de classe (les propriĂ©tĂ©s static) et les superglobales, mais pas le reste. ConcrĂštement :

  • une constante créée avec define() au milieu d’une requĂȘte existe toujours ensuite, et un second define() du mĂȘme nom Ă©chouera ;
  • un fichier ouvert, une connexion Ă  la base ou un handle cURL restent ouverts, avec leurs rĂ©glages ;
  • une extension PHP Ă©crite en C peut garder sa propre mĂ©moire interne, que Qbix ne voit pas.

La rĂšgle pratique : tout ce que votre code ouvre ou dĂ©clare pendant une requĂȘte, il doit le refermer ou le rĂ©initialiser lui-mĂȘme. C’est la mĂȘme discipline qu’avec Laravel Octane. Et pour le code qui ne la respecte pas, le mode fork-per-request rĂšgle la question.

OPcache. Le code réécrit est compilĂ© depuis la source transformĂ©e : la validation par fichier d’OPcache ne s’applique pas. Sans hot reload activĂ©, il faut redĂ©marrer aprĂšs chaque dĂ©ploiement.

Output buffering. Du code qui vide tous les buffers en boucle avec ob_end_flush() peut casser la capture de la réponse.

TĂąches longues. Pour les migrations et scripts longs, la doc recommande --workers=1 ou, plus simplement, la CLI classique.

MaturitĂ©. Pas de package Packagist, une petite communautĂ©, un pĂ©rimĂštre qui s’étend vite. Pour de la prod critique, je testerais d’abord sur un service secondaire.

🎉 Conclusion

Qbix Server part d’une intuition juste : le modĂšle shared-nothing de PHP colle trĂšs bien au fork en copy-on-write, et c’est la mĂ©moire par worker, plus que le CPU, qui plafonne FPM sur les apps I/O-bound. Le reset d’état par réécriture de fonctions et snapshot Reflection est astucieux, et le mode fork-per-request offre une vraie porte de sortie.

Pour une app Laravel ou Symfony en production, je reste sur FrankenPHP, plus mĂ»r et mieux outillĂ©. Mais Qbix mĂ©rite un banc d’essai si vos workers passent leur vie Ă  attendre des I/O, ou si l’idĂ©e de livrer une app PHP en un seul binaire vous parle. Et si vous le testez, gardez un Ɠil sur la mĂ©moire rĂ©elle avec un outil comme Ember ou SigNoz : c’est lĂ  que les promesses se vĂ©rifient.

🔗 Liens utiles

/faq

Questions fréquentes

Qu'est-ce que Qbix Server ?

+

Qbix Server est un serveur web open source (licence MIT) Ă©crit entiĂšrement en PHP. Il se lance avec php qbixserver.php, sert les fichiers statiques, exĂ©cute l'application PHP dans des workers forkĂ©s et gĂšre HTTP, WebSocket et SSE sur le mĂȘme port, sans nginx ni PHP-FPM.

Comment Qbix Server Ă©vite-t-il les fuites d’état entre requĂȘtes ?

+

En mode persistant (par dĂ©faut), il réécrit 27 fonctions natives (header, setcookie, session_start, ini_set
) au chargement des fichiers et restaure les propriĂ©tĂ©s statiques via un snapshot Reflection entre deux requĂȘtes. Pour le code rĂ©calcitrant, le mode fork-per-request exĂ©cute chaque requĂȘte dans un process jetable.

Laravel fonctionne-t-il avec Qbix Server ?

+

Oui, avec le preset laravel, mais dans mes tests (Laravel 13, Qbix v2.0.0) uniquement en mode fork-per-request. En mode persistant par dĂ©faut, les requĂȘtes suivant la premiĂšre Ă©chouent, car le require_once de bootstrap/app.php renvoie true dans un worker rĂ©utilisĂ©.

Qbix Server ou FrankenPHP, lequel choisir ?

+

FrankenPHP reste le choix le plus mĂ»r pour une app Laravel ou Symfony en production, avec Caddy, HTTP/3 et une large communautĂ©. Qbix Server est intĂ©ressant quand la concurrence sur des requĂȘtes I/O-bound est le goulet, ou pour distribuer une app PHP en binaire unique, mais ses benchmarks sont publiĂ©s par le projet lui-mĂȘme et mĂ©ritent d'ĂȘtre reproduits.