Aller au contenu principal
Site en cours de refonte — quelques pages peuvent bouger ou évoluer.
27 avril 2026

1 million de jobs Laravel par jour : le calcul de débit qui t’évite de sur-scaler au pif

Tu peux empiler des workers jusqu’à ce que “ça passe”… ou dimensionner proprement ton throughput et arrêter de payer pour du hasard. Voilà la méthode terrain.

11 min de lecture
39 vues
réactions
Partager :
1 million de jobs Laravel par jour : le calcul de débit qui t’évite de sur-scaler au pif

Traiter 1 million de jobs Laravel par jour, ce n’est pas un exploit. Ce qui est rare, c’est de le faire sans sur-scaler et sans se réveiller la nuit parce qu’une queue “email” a bloqué un truc critique. Et la plupart du temps, le problème vient d’un truc bête : personne ne fait le calcul de débit. On rajoute des workers, on augmente des instances, on se félicite… jusqu’au prochain pic.

Dans cet article, je te donne une façon de raisonner “ops” : dimensionnement par le peak et le p95, séparation des priorités, lissage des pics, et métriques pour savoir si ça traite ou si ça tourne dans le vide.

Le calcul de throughput que tout le monde évite : workers = peak_jps × durée_p95

La formule utile est simple : nombre de workers ≈ débit d’arrivée au pic (jobs/s) × durée d’exécution (s). C’est une manière pragmatique de dire : “combien de jobs je garde en vol en permanence pour tenir le pic”.

La nuance qui change tout, c’est que la durée d’un job, tu ne la prends pas en moyenne. Tu la prends en p95 (voire p99 sur des jobs critiques). La moyenne ment parce qu’elle écrase les cas lents. Or ce sont les cas lents qui te créent du backlog, puis du lag, puis du retry storm, puis l’incident.

Tu peux appliquer ça à Redis, SQS, database driver… peu importe. Le driver change le comportement (latence, visibilité des messages, ack, etc.), mais le raisonnement “arrivée vs service” reste le même.

1 million de jobs/jour : ce que ça veut dire en vrai (spoiler : ce n’est pas 11,6 jobs/s)

1 million par jour, ça fait environ 11,6 jobs/seconde en moyenne. Et c’est exactement comme ça qu’on se plante : on dimensionne pour 12 jobs/s et on découvre en prod que le vrai sujet, c’est le pic de 2 minutes après une campagne, un import, une réconciliation, ou juste un cron qui fan-out.

Un exemple réaliste : tu as un pic à 200 jobs/s pendant 10 minutes, et tes jobs font 250 ms en p95. Le calcul donne 200 × 0,25 = 50 workers pour “tenir” ce pic sans accumuler. En-dessous, tu accumules du retard. Au-dessus, tu traites plus vite… jusqu’au moment où tu te mets à saturer autre chose (DB, API externe, CPU, connexions).

Ce calcul ne te donne pas “le chiffre magique”, il te donne un ordre de grandeur défendable. Ensuite tu ajustes avec de la marge (par exemple +20 à +40% selon ton risque), et surtout tu sépares les priorités pour ne pas être obligé de tout sur-provisionner “au cas où”.

Pourquoi raisonner en p95 (et pas en moyenne) évite les fausses bonnes victoires

En prod, le job “moyen” est souvent rapide. Celui qui te flingue, c’est le job “rare” qui tombe sur une DB un peu chaude, une requête qui lock, une API externe qui rame, un garbage collector qui se réveille, un cold start, ou un payload exceptionnel. Ce job-là se moque de ta moyenne.

Et il y a un piège classique : tu augmentes les workers, tu vois le backlog baisser, tu te dis que c’est réglé. Sauf que tu viens juste d’augmenter la pression sur la base, sur Redis, sur ton service mail, sur ton storage… donc tu as peut-être rendu le p95 encore pire. Résultat : tu as payé plus cher pour déplacer le goulot d’étranglement.

Le réflexe sain : mesurer la durée de job en distribution (p50/p95/p99), pas juste un “avg runtime”. Et quand le p95 grimpe avec le nombre de workers, c’est un signal fort : tu n’es pas en train de “scaler”, tu es en train de créer de la contention.

Séparer les queues par priorité : le vrai levier pour arrêter de sur-provisionner

Si tu as une seule queue “default” où tu mets tout, tu finis toujours au même endroit : tu dimensionnes pour le pire, parce que tu ne peux pas protéger le critique. Et le jour où ça chauffe, un job “low” (thumbnail, email marketing, exports) prend la place d’un job “critical” (paiement, facturation, provisioning).

En pratique, je vise souvent quatre niveaux : critical, high, default, low. Pas pour faire joli. Pour pouvoir allouer des pools de workers différents, avec des comportements différents. Le pool critical a de la marge, des timeouts propres, des retries contrôlés, et une visibilité maximale. Le pool low, lui, est sacrifiable. S’il prend du retard, ce n’est pas agréable, mais ce n’est pas un incident.

Avec Laravel Horizon, ça se traduit par des supervisors séparés, chacun écoutant une ou plusieurs queues, avec son nombre de processes. L’idée importante : tu ne “scales” pas Laravel, tu scales des classes de risque.

// config/horizon.php (extrait simplifié)

'environments' => [
    'production' => [
        'supervisor-critical' => [
            'connection' => 'redis',
            'queue' => ['critical', 'high'],
            'balance' => 'auto',
            'minProcesses' => 10,
            'maxProcesses' => 80,
            'tries' => 3,
            'timeout' => 30,
        ],

        'supervisor-default' => [
            'connection' => 'redis',
            'queue' => ['default'],
            'balance' => 'auto',
            'minProcesses' => 5,
            'maxProcesses' => 40,
            'tries' => 2,
            'timeout' => 60,
        ],

        'supervisor-low' => [
            'connection' => 'redis',
            'queue' => ['low'],
            'balance' => 'simple',
            'processes' => 5,
            'tries' => 1,
            'timeout' => 120,
        ],
    ],
],

Lisser les pics : retries, backoff, rate limiting… sinon tu scales du chaos

Il y a une croyance tenace : “si ça retarde, on ajoute des workers”. Parfois ça marche. Souvent, tu viens juste de donner plus de munitions au chaos.

Les retries sont un bon exemple. Un système qui échoue à cause d’un service externe qui ralentit, et qui retry immédiatement en masse, ne “rattrape” pas. Il amplifie la panne. Donc le backoff n’est pas un luxe, c’est une ceinture de sécurité. Et il doit être cohérent avec le système d’en face. Si ton provider email te rate-limit à 10 req/s, mettre 200 workers ne va pas “accélérer”. Ça va juste te faire échouer plus vite.

Le rate limiting côté queue est souvent sous-utilisé. Pourtant, c’est l’un des meilleurs outils pour protéger ta DB et tes dépendances. L’objectif n’est pas d’être rapide, l’objectif est d’être stable et de garder une latence de traitement acceptable.

// Exemple : limiter un job qui tape une API externe
// (idée : éviter le "thundering herd" quand tu scales les workers)

use Illuminate\Support\Facades\RateLimiter;

public function handle(): void
{
    RateLimiter::attempt(
        key: 'provider-x',
        maxAttempts: 50, // sur la fenêtre
        callback: function () {
            // appeler l'API ici
        },
        decaySeconds: 1
    );
}

Jobs idempotents : la condition pour que le scaling ne transforme pas un bug en catastrophe

Un système de queue en prod, c’est une machine à rejouer. Entre les timeouts, les retries, les redélivrances, les déploiements, les crashs worker, tu vas exécuter deux fois le même job. Pas toujours, mais suffisamment pour que ce soit “normal”.

Donc si ton job n’est pas idempotent, tu es en train de jouer à la roulette. Le cas typique : “créer une facture”, “envoyer un email”, “créditer un compte”, “provisionner une ressource”. Si tu ne verrouilles pas ton write (clé unique, transaction, outbox pattern, job unique, check d’état), le scaling augmente mécaniquement la probabilité de doublons et d’incohérences. Et ça, c’est le genre d’incident où tu ne peux pas juste “rejouer”. Tu dois réparer des données.

Mon avis est assez sec là-dessus : si ton système n’est pas idempotent sur les actions à effet, arrête de scaler et commence par rendre tes jobs sûrs. Sinon tu dépenses de l’argent pour aller plus vite vers un mur.

Batch (Bus::batch) : vrai accélérateur ou cache-misère ?

Le batching peut être excellent quand tu fais du fan-out contrôlé, des imports chunkés, des traitements parallélisables, ou quand tu as besoin d’un suivi propre “tout ou rien” côté produit. Ça apporte aussi un vrai confort d’observabilité et de coordination, surtout si tu as des étapes (préparer → traiter → finaliser).

Mais il ne faut pas se raconter d’histoires : un batch ne réduit pas le travail total. Il peut réduire certains overheads (moins de round-trips, meilleure structuration, meilleure gestion des erreurs), mais si ton problème est “chaque job fait une requête SQL lente” ou “on sature l’API partenaire”, mettre Bus::batch par-dessus ne règle rien. Ça peut même aggraver les pics si tu lances un batch énorme sans garde-fous.

Le bon usage, c’est quand tu as un besoin produit (suivi, progression, finalisation) et que tu maîtrises le débit. Le mauvais usage, c’est quand tu veux juste “faire plus vite” sans avoir compris ton goulot.

Mesure et alerting : savoir “ça traite” vs “ça tourne”

Horizon te donne déjà une base, mais tu dois décider ce que tu veux piloter. Pour moi, les métriques qui comptent sont celles qui répondent à une question simple : est-ce que le système absorbe l’arrivée, ou est-ce qu’il accumule ?

Concrètement, je surveille le lag (temps d’attente avant exécution), le backlog (queue length), le débit jobs/s traité, et la distribution de durée (p95/p99). J’ajoute les erreurs et le taux de retry, parce que c’est souvent le premier signal d’un service externe qui part en sucette. Et j’ouvre toujours un œil sur la saturation “non-queue” : CPU, mémoire, connexions DB, locks, latence Redis. Un système de workers peut être “vert” côté queue tout en étant en train de mettre la base à genoux.

Le piège qui coûte cher, c’est l’alerting sur “nombre de jobs en attente” sans contexte. Une queue peut être longue et pourtant saine si tu sais qu’elle est low priority, drainable, et que le lag reste sous contrôle. À l’inverse, une queue peut être courte mais dangereuse si tu vois le p95 exploser et les retries grimper.

Erreurs fréquentes que je vois quand ça scale mal

La plus classique : confondre “plus de workers” avec “plus de throughput”. Si ton job est bound par la DB, tu vas juste augmenter la contention. Tu vas voir des temps de job augmenter, donc tu vas rajouter des workers, donc tu vas empirer… et tu t’étonnes que ça ne se stabilise pas. Le bon move est souvent d’optimiser le job (requêtes, indexes, éviter le N+1, réduire les payloads), ou de découpler des écritures (outbox, buffer), ou de mettre un rate limit sur ce qui tape fort.

Deuxième erreur : mélanger du critique et du non-critique dans la même queue, puis essayer de “compenser” avec du scaling global. Ça finit en facture cloud et en incidents bizarres. Séparer les priorités, c’est une façon simple de réduire ton besoin de capacité “globale”.

Troisième erreur : ignorer les timeouts et les retries. Un timeout trop court sur un job qui fait parfois 2 secondes, c’est un générateur de doublons et de backlog. Un retry immédiat sur une dépendance qui rate-limit, c’est un accélérateur d’incident. Si tu veux scaler proprement, tu dois avoir des paramètres cohérents avec la réalité de tes dépendances.

Conclusion : dimensionner, isoler, lisser… puis seulement scaler

Le “1 million de jobs/jour”, en vrai, ce n’est pas le sujet. Le sujet, c’est ton pic, ton p95, et ta capacité à protéger ce qui est critique. Si tu fais le calcul de throughput, que tu sépares tes queues par priorité, et que tu lisses les pics (retries/backoff/rate limit + idempotence), tu te retrouves avec un système qui scale sans drama… et une facture qui ressemble à quelque chose.

La suite logique, si tu veux aller plus loin, c’est de faire un test de charge ciblé sur un job (celui qui coûte cher) et de mesurer où ça casse vraiment. Très souvent, tu découvres que ton goulot n’était pas “la queue”, mais “tout ce qu’elle touche”.

Sources

Cet article vous a plu ?

Commentaires

Laisser un commentaire

Entre 10 et 2000 caractères

Les commentaires sont modérés avant publication.

Aucun commentaire pour le moment.

Soyez le premier à donner votre avis !