Comment j'ai rendu un job Sidekiq 6x plus rapide avec le keep-alive HTTP
En 2017, je travaillais sur un backend Rails avec Sidekiq. Nous faisions partie des équipes pilotes de l'API Bing Ads. Notre synchro était lente pour une raison que je n'attendais pas : l'ouverture des connexions.
Un job, c'était un compte client. Chaque job faisait environ 40 requêtes HTTPS vers Microsoft, la plupart pour envoyer de gros fichiers XML.
Une synchro complète, c'était 3 000 jobs. Donc 120 000 requêtes. Plusieurs fois par jour.
OK en test, bloqué en production
On testait avec des lots de 10 à 20 jobs. Tout marchait.
Puis la production. Boum. Les jobs s'empilaient dans la file et une synchro complète n'en finissait plus.
Mes suspects :
- La concurrence de Sidekiq qui bloque des jobs.
- Une fuite mémoire. Mais aucune alerte mémoire.
- Une requête qui réessaie en boucle et bloque les autres.
J'ai chronométré la génération du XML : 15 ms. J'ai ajouté HTTPLog : l'appel HTTP prenait 44 ms. Rien de lent.
Le log qui a tout révélé
Je ne sais plus pourquoi, mais j'ai activé toutes les options de HTTPLog :
HttpLog.configure do |config|
config.log_connect = true
config.log_request = true
config.log_data = true
config.log_status = true
config.log_response = true
config.log_benchmark = true
end
Et j'ai vu ça :
[2017-10-09-10:34:33.15] [httplog] Connecting: bulk.api.bingads.microsoft.com:443
[2017-10-09-10:34:33.75] [httplog] Sending: POST https://bulk.api.bingads.microsoft.com/...
[2017-10-09-10:34:33.77] [httplog] Status: 200
Vous l'avez vu ?
600 ms entre « Connecting » et « Sending ». Avant qu'un seul octet de la requête ne quitte notre serveur. Les 44 ms mesurées avant ne les comptaient pas.
Ce qui se passe pendant ces 600 ms
Chaque requête ouvrait une nouvelle connexion. En HTTPS, ça veut dire deux handshakes avant de faire quoi que ce soit d'utile :
- TCP : SYN, SYN-ACK. Un aller-retour.
- TLS 1.2, ce que tout le monde utilisait en 2017 : certificat, échange de clés. Deux allers-retours de plus.
Ensuite, la requête elle-même : encore un aller-retour.
Je l'ai remesuré cette semaine, sur un serveur à 96 ms :
Ouvrir la connexion, c'est 3 allers-retours. D'un serveur en Europe vers un serveur sur la côte ouest des États-Unis, un aller-retour prend 150 à 200 ms. Trois : 450 à 600 ms. Et on ne contrôlait pas l'autre côté. C'était Microsoft.
Le calcul
On avait 10 processus Sidekiq avec 3 threads chacun. 30 jobs en même temps.
Chaque requête : 600 ms pour ouvrir la connexion, environ 100 ms de vrai travail. 700 ms.
- Un job : 40 × 0,7 s = 28 s.
- Synchro complète : 3 000 jobs / 30 threads = 100 jobs par thread. 100 × 28 s = 47 min.
86 % de ce temps, nos threads attendaient des handshakes.
Et les tests ne pouvaient pas le montrer. 10 jobs sur 30 threads, c'est un seul tour : 28 s. Lent, mais rien ne semblait cassé.
La solution : garder la socket ouverte
Je suis passé à Excon parce qu'il a une option persistent :
connection = Excon.new("https://bulk.api.bingads.microsoft.com", persistent: true)
requests.each do |request|
connection.post(path: request.path, body: request.xml) # même socket à chaque fois
end
Une connexion par job. 40 requêtes dessus. Un handshake au lieu de 40.
- Un job : 0,6 s + 40 × 0,1 s = 4,6 s au lieu de 28 s.
- Synchro complète : 100 × 4,6 s = 7,7 min au lieu de 47.
6x plus rapide. De 120 000 handshakes par synchro à 3 000.
Et il faut aussi compter le gain du slow start sur les gros XML
Ces gros envois de XML avaient un deuxième problème. Une nouvelle connexion TCP démarre lentement.
L'émetteur ne connaît pas votre bande passante, alors il démarre avec environ 14 Ko en vol et double à chaque aller-retour : 14, 28, 57, 114 Ko... Une connexion neuve a besoin de plusieurs allers-retours pour transférer un gros fichier. Une connexion chaude a déjà pris de la vitesse.
Voici 1 Mo qui arrive de Singapour, aller-retour par aller-retour :
Et le même 1 Mo depuis différentes distances :
Réutiliser la connexion a aussi réglé ça. Je ne le savais même pas à l'époque.
Un piège sous Linux : la fenêtre rétrécit à nouveau quand la connexion reste inactive. Sur les serveurs Sidekiq, net.ipv4.tcp_slow_start_after_idle=0 la garde chaude.
Est-ce toujours vrai en 2026 ?
J'ai écrit un petit script Python et mesuré 11 API de LLM (OpenAI, Anthropic, OpenRouter, Gemini, Mistral, Groq et d'autres), plus quelques serveurs lointains. 20 requêtes à la suite : une nouvelle connexion à chaque fois, puis une seule connexion.
Deux choses ont changé depuis 2017. Les API sont derrière un CDN à quelques ms de vous, et TLS 1.3 n'a besoin que d'un aller-retour au lieu de deux. Ouvrir une connexion vers OpenAI ou Anthropic coûte donc environ 20 ms depuis mon bureau.
Quand le serveur met plus de 100 ms à répondre, ça représente 7 % à 26 % de chaque requête. Quand il est rapide, le handshake en est la plus grosse partie : OpenRouter passe de 45 ms à 14 ms par requête.
Et un serveur lointain, encore en TLS 1.2 ? 4,2× plus rapide avec une seule connexion. Même histoire qu'en 2017.
Ce que je fais maintenant
- Un client HTTP par thread, réutilisé. En Ruby :
Net::HTTP.startavec un bloc,net-http-persistent, HTTPX, ou Excon avecpersistent: true. En Python :requests.Sessionouhttpx.Client. - Créez le client OpenAI ou Anthropic une seule fois. Il contient le pool de connexions. Un nouveau client par requête le jette à la poubelle.
- Loggez le temps de connexion, pas seulement celui de la requête. C'est là que mes 600 ms se cachaient.
- Testez avec le volume de production. 10 jobs ne vous montreront jamais ça.