Restons en contact

Social

Registre du labEXP-02 · Performance
ConcluantEXP-02iones lab

Node, Go, Rust, PHP : une même API, quatre runtimes, des chiffres

La questionÀ partir de quel niveau de charge le choix du runtime devient-il le facteur limitant d’une API métier classique ?

Verdict

Sur une API avec base de données, l’écart entre runtimes est de 1 à 4 en débit brut, mais la base sature bien avant. Le runtime ne devient le goulot qu’au-delà de 3 000 requêtes par seconde par instance, un seuil que la plupart de nos clients n’approchent pas.

DécisionStack PHP 8.3 et Node conservées comme socle. Go retenu pour les workers à forte concurrence. Rust écarté pour le développement métier.

Statut
Concluant
Durée
3 semaines
Équipe
3 développeurs, 1 architecte
Stack
  • Node 22
  • Go 1.23
  • Rust (Axum)
  • PHP 8.3
  • FrankenPHP
  • Postgres
  • k6
38 400req/s · Rust, sans baseGo 31 200 · Node 12 900 · PHP 9 800
4 100req/s · tous runtimes, avec baseplafond fixé par Postgres
×3,2temps de développement Rust vs PHPsur le même périmètre
41 Momémoire par instance GoRust 18 · Node 96 · PHP 118

Contexte

« Il faudrait réécrire ça en Go. » La phrase revient à chaque projet qui ralentit. Elle est parfois juste, souvent coûteuse, et rarement étayée par une mesure. Nous voulions des chiffres à nous, sur une API qui ressemble à celles que nous livrons : authentification, validation, lecture et écriture en base, sérialisation JSON. Pas un « hello world ».

Ce qu’on a testé

Une API de gestion de commandes à 6 endpoints (liste paginée, détail, création avec validation, mise à jour, recherche, export) implémentée quatre fois par quatre développeurs différents, chacun dans le runtime qu’il maîtrise le mieux, avec la même base Postgres et le même jeu de données de 2 millions de lignes.
  • Node 22 avec Fastify.
  • Go 1.23 avec la bibliothèque standard et pgx.
  • Rust avec Axum et sqlx.
  • PHP 8.3 avec Symfony, servi par FrankenPHP en mode worker.
Trois scénarios de charge avec k6, sur une machine identique de 4 vCPU : parsing et sérialisation JSON sans base, lecture avec base, et scénario mixte réaliste (80 % lecture, 20 % écriture). Nous avons mesuré le débit, la latence p50 et p99, la mémoire, et chronométré le temps de développement de chaque implémentation.

Résultats

38 400

req/s · Rust · sans base

31 200

req/s · Go · sans base

12 900

req/s · Node · sans base

9 800

req/s · PHP · sans base

Sans base de données, la hiérarchie attendue se confirme : Rust et Go loin devant, Node et PHP à un niveau comparable. L’écart de 1 à 4 est réel, et la mémoire suit la même courbe.
Avec base de données, tout le monde se retrouve au même endroit. Sur le scénario mixte, les quatre implémentations plafonnent entre 3 800 et 4 100 requêtes par seconde : c’est Postgres qui sature, sur les mêmes requêtes et les mêmes index. La latence p99 reste sous 45 ms pour Go et Rust, 70 ms pour Node, 85 ms pour PHP, ce qui est invisible pour un utilisateur derrière un réseau.
  • FrankenPHP en mode worker change la donne pour PHP : trois fois plus de débit qu’en PHP-FPM classique sur le même code, sans modification applicative.
  • Temps de développement : PHP 11 h, Node 13 h, Go 19 h, Rust 35 h pour le même périmètre, tests inclus. Rust a demandé deux relectures de plus pour être considéré comme propre.
  • Sous 1 000 requêtes par seconde, aucun runtime n’est distinguable d’un autre sur la latence perçue.
  • Go se démarque sur les tâches de fond à forte concurrence (export, traitement par lots) : 40 Mo de mémoire et un modèle de concurrence simple à relire.

Ce qu’on en conclut

Changer de runtime pour la performance n’a de sens que si le runtime est réellement le goulot, et sur une API métier classique, il ne l’est presque jamais. La base, les requêtes, le cache et le réseau arrivent bien avant. Une réécriture coûte des semaines et fait perdre l’expertise de l’équipe sur son propre code ; un index manquant se corrige en une heure.
Cela ne veut pas dire que le choix est neutre. Go offre un vrai avantage sur les workers et les services très sollicités, à un coût de développement raisonnable. Rust est excellent mais son surcoût de développement ne se justifie pas sur du métier ordinaire. PHP 8.3 avec FrankenPHP reste un socle parfaitement légitime, et c’est celui sur lequel nos équipes sont les plus productives.

Et maintenant

  • FrankenPHP est devenu notre configuration recommandée pour les nouveaux projets PHP et les migrations depuis PHP-FPM.
  • Go est notre choix par défaut pour les workers de traitement (exports, files d’attente, intégrations) quand le volume le justifie.
  • Les demandes de réécriture pour cause de performance passent désormais par un profilage préalable, avec ce banc comme référence.