Faire évoluer Node.js au-delà de 10 M de requêtes/jour : ce qui compte vraiment
Tous les ArticlesDéveloppement

Faire évoluer Node.js au-delà de 10 M de requêtes/jour : ce qui compte vraiment

Prixelo StudioPrixelo Studio
Jan 12, 2026 7 min

Node.js à l'échelle : la version sans fioritures

La plupart des articles sur la mise à l'échelle de Node.js sont écrits par des gens qui ont fait évoluer un seul service jusqu'à un million de requêtes par jour, et qui supposent que ça se généralise. Nous avons exploité plusieurs systèmes au-delà de 10 millions de requêtes par jour, et les leçons qu'on en tire n'ont rien de glamour.

Voici le guide qu'on aurait aimé avoir il y a quatre ans.

Arrêtez de débattre des microservices

L'erreur la plus coûteuse que nous ayons vu des équipes commettre, c'est de découper en microservices avant d'y être obligées. Un monolithe avec des frontières de modules propres tient à l'échelle bien plus loin que ce que la plupart des ingénieurs imaginent, et il coûte un ordre de grandeur moins cher à exploiter.

Notre règle : rester monolithique tant qu'aucune de ces conditions n'est vraie.

  • Un chemin spécifique a des caractéristiques de mise à l'échelle différentes du reste (par exemple un endpoint d'inférence ML lourd).
  • Un chemin spécifique a un rythme de déploiement différent (par exemple du code soumis à des contraintes de conformité qui doit sortir mensuellement pendant que le reste sort quotidiennement).
  • L'équipe dépasse 25 ingénieurs et les conflits de merge deviennent un vrai problème.

Si aucune de ces conditions n'est vraie, découper en services ajoute du coût opérationnel — service discovery, tracing distribué, orchestration de déploiement, modes de défaillance réseau — sans rien apporter que vous ne pourriez obtenir avec un monorepo propre.

Le vrai goulot d'étranglement, c'est la base de données

Dans tous les projets de mise à l'échelle Node.js que nous avons menés, le goulot d'étranglement finit par se déplacer vers la base de données. Le CPU et la mémoire des services Node.js sont généralement corrects, parce que Node gère bien la concurrence I/O. Les problèmes viennent d'ailleurs :

  • Des pools de connexions non bornés qui font fondre Postgres
  • Un index unique très sollicité qui devient un goulot d'étranglement en écriture
  • Des requêtes N+1 dans les ORM, qui paraissent inoffensives en revue de code
  • Des transactions longues qui retiennent des verrous pendant l'exécution de la logique métier
  • Du trafic de lecture envoyé sur le primaire au lieu d'une réplique de lecture

Premières mesures concrètes :

  1. PgBouncer ou équivalent devant Postgres, en mode transaction pooling, avec un plafond de connexions basé sur le CPU réel de la base de données, pas sur une intuition côté Node.js.
  2. Des répliques de lecture avec un routage explicite. Ne laissez pas l'ORM décider. Ayez deux clients dans le code : db.read et db.write. Obligez chaque développeur à choisir.
  3. La mesure du temps des requêtes en production. Pino associé à un logger de requêtes lentes. Vous voulez une alerte Slack dès qu'une requête dépasse 200 ms.
  4. EXPLAIN ANALYZE en CI pour les nouvelles requêtes. Nous l'ajoutons comme vérification de PR sur les changements qui touchent au schéma.

Le cache, en trois couches

Le cache vous sauve dans cet ordre : CDN, mémoire locale, Redis. Sautez une couche et vous le regretterez.

CDN. Tout ce qui peut être mis en cache en périphérie doit l'être. Cela inclut les réponses d'API pour les lectures non authentifiées. Pas besoin de faire compliqué — des en-têtes Cache-Control et un CDN qui les respecte règlent gratuitement 60 % du trafic de lecture.

Mémoire locale. Un petit cache LRU (nous utilisons lru-cache) devant Redis capture la longue traîne des requêtes répétées au sein d'un même processus. Ça paraît anodin ; dans nos profils, ça économise 30 à 50 % du trafic Redis.

Redis. Pour l'état partagé et les valeurs calculées qui doivent survivre entre les processus. Utilisez-le de façon délibérée — chaque clé doit avoir un propriétaire, un TTL et une stratégie d'invalidation documentée. Ce sont les clés Redis non suivies qui font qu'un cache de production finit par servir des données vieilles de 6 mois le jour du Black Friday.

Rendez asynchrone tout ce qui peut l'être

Les chemins de requête synchrones doivent faire le minimum : valider, persister, répondre. Tout le reste — emails, déclenchement de webhooks, traitement d'images, appels à des API tierces, indexation pour la recherche — part dans une file d'attente.

Nous utilisons BullMQ sur Redis pour presque tout. Pour deux raisons :

  1. L'isolation des pannes. Une API tierce capricieuse ne peut pas faire tomber votre chemin de requête si l'appel se produit dans un worker.
  2. La backpressure. Quand le trafic explose, la file absorbe le pic et les workers l'écoulent à un rythme soutenable. Le code synchrone, lui, s'effondre tout simplement sous la charge.

Exemple concret : un tunnel de paiement que nous avons reconstruit pour un client déclenchait auparavant 6 appels tiers en ligne (taxes, fraude, exécution des commandes, email, SMS, analytics). Le paiement médian prenait 1,4 seconde, et le p99 montait à 8 secondes dès qu'un prestataire ralentissait. Après avoir déplacé tout, sauf les taxes et la fraude, vers des workers BullMQ, la médiane est tombée à 220 ms et le p99 est resté sous 600 ms.

L'observabilité avant l'architecture ingénieuse

Face à la montée en charge, le réflexe est de concevoir des systèmes ingénieux. La bonne approche consiste à instrumenter le système existant jusqu'à voir exactement où le temps est dépensé.

Nous standardisons sur :

  • Le tracing OpenTelemetry, exporté vers Honeycomb ou Tempo. Chaque requête a un trace ID. Chaque appel externe est un span.
  • Des logs structurés, au format JSON, avec le trace ID. Pino fait très bien l'affaire.
  • Des métriques RED par endpoint — Rate, Errors, Duration (taux, erreurs, durée). Des tableaux de bord Grafana. Des alertes sur le p99, pas sur le p50.
  • Des contrôles synthétiques depuis l'extérieur de votre réseau, exécutés chaque minute sur les chemins critiques.

Vous ne prédirez pas correctement vos goulots d'étranglement. Vous les mesurerez. L'équipe qui livre l'observabilité en premier gagne les six mois suivants de travail de mise à l'échelle.

Les pièges que nous avons vus revenir sans cesse

Une courte liste de choses que nous avons vues faire tomber la production :

  • Trop logger, faire tomber le pipeline de logs, puis se retrouver sans aucun log.
  • Oublier de définir le --max-old-space-size de Node. La valeur par défaut est de 1,5 Go. Votre conteneur en a 8. Faites le calcul.
  • Écrire dans une base de données depuis une boucle Promise.all sans borner la concurrence. Épuisement du pool de connexions au pire moment possible.
  • Des health checks qui passent au vert alors que le service est réellement cassé. Testez la chaîne de dépendances, pas seulement le processus.
  • Faire confiance à un SDK tiers pour gérer les timeouts. Encadrez-le toujours avec les vôtres.

La vérité sans éclat

À 10 millions de requêtes par jour, la différence entre un système qui tient et un système qui s'effondre tient rarement à un génie architectural. C'est une équipe qui surveille ses tableaux de bord, corrige les requêtes lentes dans la semaine où elles apparaissent, mène ses retours d'incident honnêtement, et résiste à l'envie de refactoriser quand elle devrait plutôt instrumenter.

Avec cette culture bien en place, Node.js tient bien plus loin que ce que laissent penser les conférences. C'est exactement le type de travail que nos équipes backend et cloud & DevOps mènent pour des produits en phase de croissance.

Partager cet article
Prixelo Studio

Prixelo Studio

Notes from the studio on craft, code, and product.