Jour 46 Day 46 · mardi 13 octobre 2026 Tuesday 13 October 2026 Architecture Avancé
Scaler une app : reverse proxy, load balancing & haute dispo Scaling an app: reverse proxy, load balancing & high availability
Vertical vs horizontal, stateless, nginx, health checks, failover : comment une app encaisse la charge et survit aux pannes — le sujet d'architecture qui distingue les candidats en entretien. Vertical vs horizontal, stateless, nginx, health checks, failover: how an app absorbs load and survives failures — the architecture topic that sets candidates apart in interviews.
L’essentiel
Scaler, c’est répondre à deux questions distinctes qu’on confond souvent : encaisser plus de trafic (performance) et survivre aux pannes (haute disponibilité). Les deux se résolvent avec les mêmes briques : un reverse proxy devant, plusieurs instances derrière, et la chasse méthodique aux points uniques de défaillance.
Deux stratégies de scaling :
| Vertical (scale up) | Horizontal (scale out) | |
|---|---|---|
| Principe | Une machine plus grosse (CPU, RAM) | Plus de machines identiques |
| Effort | Zéro changement de code | Stateless requis + load balancer |
| Limite | Plafond matériel, prix exponentiel | Quasi illimité |
| Dispo | SPOF inchangé : une seule machine | La panne d’une instance est absorbée |
| Déploiement | Souvent un redémarrage | Rolling deploy sans coupure |
Le vertical d’abord : c’est la solution la plus simple et souvent suffisante. L’horizontal quand on a besoin de disponibilité (plusieurs instances = tolérance de panne) ou qu’on approche le plafond d’une machine.
💡 L’honnêteté qui marque des points — un VPS correct (8 vCPU, 16 Go) encaisse des milliers de requêtes/seconde sur une app bien écrite : Stack Overflow a longtemps servi sa planète depuis une poignée de serveurs. En entretien, dire « je commence par un monolithe sur une machine, je mesure, et je scale quand les chiffres le demandent » vaut mieux que dessiner Kubernetes au tableau pour 200 utilisateurs.
Comment ça marche
Le prérequis absolu du scaling horizontal : le stateless. Si l’instance A stocke la session de l’utilisateur en mémoire, la requête suivante routée vers B le déconnecte. Tout état partagé sort donc du processus : sessions dans Redis (ou JWT signé côté client), uploads dans un object storage (S3), la vérité en base. Test simple : une instance doit pouvoir mourir à tout instant sans qu’aucun utilisateur ne le remarque.
L’architecture cible :
┌─────────────┐
Internet ──▶│ LB / nginx │ TLS, gzip, health checks
└──────┬──────┘
┌──────────┼──────────┐
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│ app1 │ │ app2 │ │ app3 │ (stateless)
└───┬──┘ └───┬──┘ └───┬──┘
└──────────┼──────────┘
┌─────────┴─────────┐
▼ ▼
┌───────────┐ ┌────────────┐
│ Redis │ │ PG primary │──▶ replica
│ (sessions)│ └────────────┘
└───────────┘
Le reverse proxy (nginx, Traefik, Caddy, HAProxy) est la porte d’entrée unique, et il fait bien plus que transmettre :
- TLS termination : le HTTPS s’arrête au proxy, les instances parlent HTTP en interne — un seul endroit où gérer les certificats (Let’s Encrypt).
- Compression (gzip/brotli), cache du statique, en-têtes (
X-Forwarded-Forpour conserver l’IP réelle du client). - Routing :
api.exemple.com→ backend,/static→ fichiers. C’est exactement le rôle de Traefik dans Coolify : un proxy, N apps derrière.
Le load balancer est un reverse proxy qui distribue vers N instances. Algorithmes à connaître : round-robin (chacun son tour, le défaut), least-connections (vers l’instance la moins chargée — meilleur quand les requêtes ont des durées inégales), hash d’IP (même client → même instance). Et surtout les health checks : le LB sonde chaque instance et sort du pool celles qui ne répondent plus. C’est lui qui transforme « une instance est morte » en « personne ne l’a remarqué ».
upstream app {
least_conn; # vers l'instance la moins chargée
server 10.0.0.11:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.12:3000 max_fails=3 fail_timeout=30s;
# 3 échecs consécutifs → l'instance sort du pool pendant 30 s
}
server {
listen 443 ssl http2;
server_name app.exemple.com;
ssl_certificate /etc/letsencrypt/live/app/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app/privkey.pem;
gzip on; # compression au proxy, pas dans l'app
location / {
proxy_pass http://app; # → l'upstream défini plus haut
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # l'app sait qu'on est en HTTPS
}
}
Sticky sessions : le LB colle un client à une instance donnée (via cookie). C’est une béquille pour apps stateful, à éviter : la charge se répartit mal, et la mort d’une instance déconnecte d’un coup tous ses clients. La vraie solution est de rendre l’app stateless — les sticky sessions ne servent qu’à gagner du temps sur du legacy.
Concepts clés à maîtriser
- SPOF (single point of failure) : tout composant unique dont la panne emporte le système. On les traque étage par étage : 1 instance → N instances ; 1 DB → primary + replica ; 1 LB → 2 LB avec IP flottante (keepalived/VRRP) ou failover DNS. La haute dispo se vérifie composant par composant, jamais globalement.
- Réplication de la base : le primary encaisse les écritures et streame ses changements vers des replicas qui servent les lectures (la majorité du trafic). Attention au replication lag : une lecture sur replica juste après une écriture peut renvoyer l’ancien état.
- Failover : promotion d’un replica en primary quand le primary meurt. Automatisé (Patroni pour PostgreSQL) ou manuel — toujours plus délicat qu’il n’y paraît : si l’ancien primary revient sans savoir qu’il a été remplacé, on obtient un split-brain (deux serveurs qui acceptent des écritures).
- CDN : le statique (images, JS, CSS) servi depuis des points de présence proches des utilisateurs (Cloudflare, CloudFront). Décharge l’origine de la majorité des requêtes et écrase la latence mondiale. Premier réflexe de scaling pour un site à fort contenu statique.
- Serverless (en survol) : l’extrême du scaling horizontal — la plateforme (Lambda, Cloud Run) instancie la fonction à la demande, de zéro à des milliers. Contreparties : cold starts, coût élevé en charge soutenue, lock-in. Excellent pour des charges en pics, pas une fin en soi.
- Mesurer avant de scaler : le goulot réel est rarement où on croit — souvent une requête SQL sans index bien avant le CPU. Load testing (k6, wrk), métriques (APM), puis on scale ce qui sature. Doubler les instances ne répare pas une requête N+1.
🎤 En entretien — « Ton app tombe en prod, par où tu commences ? » Réponse structurée : 1) constater et communiquer ; 2) lire la stack du haut vers le bas — le LB (taux de 5xx ? combien d’instances encore dans le pool ?), les instances (CPU, RAM, OOM kill ?), la base (connexions saturées ? requête lente ?) ; 3) mitiger d’abord (rollback du dernier déploiement, redémarrage, scale up), comprendre ensuite (post-mortem). Le réflexe « le dernier changement déployé est le suspect n°1 » montre de l’expérience réelle.
En entretien
« Vertical ou horizontal : comment tu choisis ? » — Vertical d’abord : zéro complexité, on grossit la machine, et ça suffit très longtemps. Horizontal quand on veut de la haute dispo (N instances = tolérance de panne) ou qu’on approche le plafond d’une machine. Le point clé à placer : l’horizontal exige le stateless — c’est un travail sur l’app avant d’être un travail d’infra.
« Pourquoi le stateless est-il indispensable au scaling horizontal ? » — Parce que le LB route chaque requête vers n’importe quelle instance : un état gardé en mémoire locale (session, cache, fichier uploadé) devient invisible pour les autres. On externalise tout : sessions dans Redis ou JWT, fichiers dans un object storage, vérité en base. Le test : « puis-je tuer n’importe quelle instance à tout instant sans impact utilisateur ? »
« Reverse proxy et load balancer, c’est pareil ? » — Un load balancer est un reverse proxy avec plusieurs backends. Le reverse proxy est la porte d’entrée : TLS termination, compression, routing, cache. Il devient load balancer dès qu’il distribue sur un pool avec un algorithme et des health checks. nginx, Traefik et HAProxy jouent les deux rôles.
« Round-robin ou least-connections ? » — Round-robin distribue équitablement en nombre de requêtes : parfait si elles se valent. Least-connections vise l’instance la moins occupée : meilleur quand les durées varient (un gros export ne bloque pas la file derrière lui). Dans les deux cas, les health checks sont non négociables : distribuer vers une instance morte, c’est distribuer des erreurs.
« Comment tu rends une base de données hautement disponible ? » — Réplication primary → replicas : les lectures se répartissent sur les replicas, et le failover promeut un replica si le primary tombe. À mentionner pour marquer des points : le replication lag (lecture obsolète juste après une écriture) et la difficulté du failover automatique (split-brain). La DB est le composant le plus dur à scaler — d’où la règle « stateless partout, l’état concentré dans la base ».
Pièges & idées reçues
⚠️ Sur-architecturer, le piège n°1 — monter Kubernetes, trois microservices et une queue pour une app à 50 utilisateurs, c’est payer aujourd’hui (complexité, ops, temps de dev) pour un problème hypothétique. La progression saine : monolithe propre → VPS costaud → LB + 2-3 instances → et seulement là, la suite. Chaque étage se franchit quand les mesures le demandent, pas par anticipation.
- « Le load balancer suffit pour la haute dispo » — non : si la base est unique, le SPOF a juste changé d’étage. Et un seul LB devant dix instances reste un SPOF. La HA se vérifie maillon par maillon.
- Sticky sessions comme « solution » au state : un pansement qui casse la répartition de charge et transforme chaque panne d’instance en déconnexions massives.
- Oublier le replication lag : lire sur un replica juste après avoir écrit sur le primary peut renvoyer l’ancien état. Les lectures critiques (« read your own writes ») vont sur le primary.
- Scaler sans mesurer : doubler les instances ne sert à rien si le goulot est une requête sans index ou un pool de connexions saturé. Mesurer, puis scaler ce qui sature.
- Confondre scaling et performance : optimiser une app lente (cache, index, requêtes) est presque toujours moins cher que multiplier les machines qui exécutent du code lent.
Pour aller plus loin
- nginx — Using nginx as HTTP load balancer — la référence, lisible en quinze minutes
- The Twelve-Factor App — les facteurs VI (processes) et VIII (concurrency) formalisent le stateless
- Traefik documentation — le reverse proxy « cloud-native » : routing par labels, Let’s Encrypt automatique
- Designing Data-Intensive Applications (Martin Kleppmann) — le chapitre 5 sur la réplication : la bible de l’architecture distribuée
- Cloudflare — What is a CDN? — clair et illustré
The essentials
Scaling answers two distinct questions people often conflate: absorbing more traffic (performance) and surviving failures (high availability). Both are solved with the same building blocks: a reverse proxy in front, several instances behind, and a methodical hunt for single points of failure.
Two scaling strategies:
| Vertical (scale up) | Horizontal (scale out) | |
|---|---|---|
| Principle | A bigger machine (CPU, RAM) | More identical machines |
| Effort | Zero code changes | Stateless required + load balancer |
| Limit | Hardware ceiling, exponential price | Nearly unlimited |
| Availability | SPOF unchanged: one machine | An instance failure is absorbed |
| Deployment | Often a restart | Zero-downtime rolling deploy |
Vertical first: it’s the simplest solution and often enough. Horizontal when you need availability (several instances = fault tolerance) or when you approach a single machine’s ceiling.
💡 The honesty that scores points — a decent VPS (8 vCPU, 16 GB) handles thousands of requests/second on a well-written app: Stack Overflow long served the planet from a handful of servers. In an interview, saying “I start with a monolith on one machine, I measure, and I scale when the numbers demand it” beats drawing Kubernetes on the whiteboard for 200 users.
How it works
The absolute prerequisite of horizontal scaling: statelessness. If instance A stores the user’s session in memory, the next request routed to B logs them out. All shared state must leave the process: sessions in Redis (or a client-side signed JWT), uploads in object storage (S3), truth in the database. Simple test: any instance must be able to die at any moment without a single user noticing.
The target architecture:
┌─────────────┐
Internet ──▶│ LB / nginx │ TLS, gzip, health checks
└──────┬──────┘
┌──────────┼──────────┐
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│ app1 │ │ app2 │ │ app3 │ (stateless)
└───┬──┘ └───┬──┘ └───┬──┘
└──────────┼──────────┘
┌─────────┴─────────┐
▼ ▼
┌───────────┐ ┌────────────┐
│ Redis │ │ PG primary │──▶ replica
│ (sessions)│ └────────────┘
└───────────┘
The reverse proxy (nginx, Traefik, Caddy, HAProxy) is the single front door, and it does far more than forward:
- TLS termination: HTTPS stops at the proxy, instances speak plain HTTP internally — one single place to manage certificates (Let’s Encrypt).
- Compression (gzip/brotli), static caching, headers (
X-Forwarded-Forto preserve the client’s real IP). - Routing:
api.example.com→ backend,/static→ files. That’s exactly Traefik’s role in Coolify: one proxy, N apps behind it.
The load balancer is a reverse proxy distributing to N instances. Algorithms to know: round-robin (each in turn, the default), least-connections (towards the least loaded instance — better when request durations vary), IP hash (same client → same instance). And above all, health checks: the LB probes each instance and removes from the pool those that stop responding. That’s what turns “an instance died” into “nobody noticed”.
upstream app {
least_conn; # towards the least loaded instance
server 10.0.0.11:3000 max_fails=3 fail_timeout=30s;
server 10.0.0.12:3000 max_fails=3 fail_timeout=30s;
# 3 consecutive failures → instance leaves the pool for 30 s
}
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app/privkey.pem;
gzip on; # compression at the proxy, not in the app
location / {
proxy_pass http://app; # → the upstream defined above
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # the app knows it's HTTPS
}
}
Sticky sessions: the LB pins a client to a given instance (via cookie). It’s a crutch for stateful apps, best avoided: load spreads unevenly, and one instance dying disconnects all its clients at once. The real solution is making the app stateless — sticky sessions only buy time on legacy code.
Key concepts to master
- SPOF (single point of failure): any unique component whose failure takes the system down. Hunt them tier by tier: 1 instance → N instances; 1 DB → primary + replica; 1 LB → 2 LBs with a floating IP (keepalived/VRRP) or DNS failover. High availability is verified component by component, never globally.
- Database replication: the primary takes the writes and streams its changes to replicas serving the reads (the majority of traffic). Beware of replication lag: a read on a replica right after a write can return the old state.
- Failover: promoting a replica to primary when the primary dies. Automated (Patroni for PostgreSQL) or manual — always trickier than it looks: if the old primary comes back not knowing it was replaced, you get a split-brain (two servers accepting writes).
- CDN: static assets (images, JS, CSS) served from points of presence close to users (Cloudflare, CloudFront). Offloads the majority of requests from the origin and crushes worldwide latency. First scaling reflex for a static-heavy site.
- Serverless (in passing): the extreme of horizontal scaling — the platform (Lambda, Cloud Run) instantiates the function on demand, from zero to thousands. Trade-offs: cold starts, high cost under sustained load, lock-in. Excellent for spiky workloads, not an end in itself.
- Measure before scaling: the real bottleneck is rarely where you think — often an unindexed SQL query long before the CPU. Load testing (k6, wrk), metrics (APM), then scale what saturates. Doubling instances doesn’t fix an N+1 query.
🎤 In an interview — “Your app goes down in production, where do you start?” Structured answer: 1) confirm and communicate; 2) read the stack top to bottom — the LB (5xx rate? how many instances left in the pool?), the instances (CPU, RAM, OOM kill?), the database (saturated connections? slow query?); 3) mitigate first (roll back the last deploy, restart, scale up), understand later (post-mortem). The reflex “the last deployed change is suspect number one” shows real experience.
In an interview
“Vertical or horizontal: how do you choose?” — Vertical first: zero complexity, you grow the machine, and it lasts a very long time. Horizontal when you want high availability (N instances = fault tolerance) or you approach a single machine’s ceiling. The key point to land: horizontal requires statelessness — it’s work on the app before it’s infrastructure work.
“Why is statelessness essential for horizontal scaling?” — Because the LB routes each request to any instance: state kept in local memory (session, cache, uploaded file) becomes invisible to the others. Externalize everything: sessions in Redis or JWT, files in object storage, truth in the database. The test: “can I kill any instance at any moment with zero user impact?”
“Are a reverse proxy and a load balancer the same thing?” — A load balancer is a reverse proxy with several backends. The reverse proxy is the front door: TLS termination, compression, routing, caching. It becomes a load balancer the moment it distributes over a pool with an algorithm and health checks. nginx, Traefik and HAProxy play both roles.
“Round-robin or least-connections?” — Round-robin distributes evenly in request count: perfect if requests are similar. Least-connections targets the least busy instance: better when durations vary (a big export doesn’t block the queue behind it). In both cases, health checks are non-negotiable: distributing to a dead instance means distributing errors.
“How do you make a database highly available?” — Primary → replica replication: reads spread over the replicas, and failover promotes a replica if the primary dies. To score points, mention replication lag (stale read right after a write) and the difficulty of automated failover (split-brain). The database is the hardest component to scale — hence the rule “stateless everywhere, state concentrated in the database”.
Pitfalls & misconceptions
⚠️ Over-architecting, trap number one — setting up Kubernetes, three microservices and a queue for an app with 50 users means paying today (complexity, ops, dev time) for a hypothetical problem. The healthy progression: clean monolith → beefy VPS → LB + 2-3 instances → and only then, the rest. You climb each tier when the measurements demand it, not in anticipation.
- “The load balancer is enough for high availability” — no: if the database is unique, the SPOF just moved down a tier. And a single LB in front of ten instances is still a SPOF. HA is verified link by link.
- Sticky sessions as a “solution” to state: a band-aid that breaks load distribution and turns every instance failure into mass disconnections.
- Forgetting replication lag: reading from a replica right after writing to the primary can return the old state. Critical reads (“read your own writes”) go to the primary.
- Scaling without measuring: doubling instances is useless if the bottleneck is an unindexed query or a saturated connection pool. Measure, then scale what saturates.
- Confusing scaling and performance: optimizing a slow app (cache, indexes, queries) is almost always cheaper than multiplying machines that run slow code.
Going further
- nginx — Using nginx as HTTP load balancer — the reference, readable in fifteen minutes
- The Twelve-Factor App — factors VI (processes) and VIII (concurrency) formalize statelessness
- Traefik documentation — the “cloud-native” reverse proxy: label-based routing, automatic Let’s Encrypt
- Designing Data-Intensive Applications (Martin Kleppmann) — chapter 5 on replication: the bible of distributed architecture
- Cloudflare — What is a CDN? — clear and illustrated