Diffuser de grandes réponses d'API : compression, exports et limites
Apprenez à diffuser de grandes réponses d'API avec gzip/brotli, exports en streaming sûrs et limites raisonnables pour que les gros rapports se téléchargent sans faire planter votre appli.

Pourquoi les grosses réponses d'API font planter les applis
Les grosses réponses échouent souvent de la même façon : le rapport fonctionne sur votre laptop avec une petite base, puis commence à timeouter, planter ou renvoyer un fichier partiel en production. Les données sont plus volumineuses, le réseau est plus lent, et le serveur gère du vrai trafic en même temps.
La plupart des applis cassent parce qu'elles construisent toute la réponse en mémoire avant d'envoyer quoi que ce soit. Un endpoint « télécharger le rapport » interroge beaucoup de lignes, les formate en JSON ou CSV, stocke le résultat entier dans un tampon, puis enfin l'envoie au client. Ça provoque des pics mémoire, déclenche le garbage collector et ralentit tout le reste.
Les mêmes points de défaillance reviennent sans cesse :
- Pics mémoire dus au buffering de la charge complète (parfois plusieurs fois lors de retries)
- Timeouts au niveau du serveur applicatif, du reverse proxy/load balancer ou du client parce que la réponse met trop de temps
- Clients lents qui lisent les données progressivement, gardent les connexions ouvertes et bloquent des workers
- Retries qui doublent la charge quand le système est déjà sous pression
- Gros JSON coûteux à sérialiser et à parser côté client
L'objectif n'est pas « faire le plus gros fichier possible ». C'est de délivrer de grandes réponses de façon fiable pour qu'un export lourd ne dégrade pas toute l'appli pour tout le monde.
On peut souvent corriger ça sans réécrire la fonctionnalité. La plupart des équipes retrouvent de la stabilité en combinant trois idées : compresser quand ça aide, streamer les données par morceaux au lieu de tout bufferiser, et appliquer des limites (taille, temps, lignes) adaptées aux besoins réels des utilisateurs.
Si vous avez hérité d'une appli générée par IA où les exports plantent ou l'auth se casse en cours de téléchargement, c'est généralement réparable avec des changements ciblés (buffering, requêtes sans bornes, backpressure manquant). FixMyMess (fixmymess.ai) voit souvent ces exports qui « marchent en dev, cassent en prod » et aide à les rendre sûrs en production.
Compression, streaming et limites : ce que chacun résout
Les grosses réponses ratent généralement pour une des trois raisons suivantes : la charge est trop volumineuse pour être transférée rapidement, trop volumineuse pour tenir en mémoire, ou trop coûteuse à générer. La compression, le streaming et les limites ciblent chacun un problème différent.
La compression réduit le poids sur le réseau. Le serveur envoie moins d'octets et le client télécharge plus vite. Cela aide surtout pour du contenu textuel (JSON, CSV). C'est moins utile quand les données sont déjà compressées (images, PDF, ZIP). La compression ne corrige pas non plus l'erreur fondamentale de construire une chaîne de 200 Mo en mémoire d'abord.
Le streaming change la façon de livrer la réponse. Au lieu de construire tout l'export puis l'envoyer, vous l'envoyez par petits morceaux au fur et à mesure que vous le produisez. C'est l'outil principal quand il faut envoyer des millions de lignes sans manquer de RAM. Le streaming maintient la mémoire stable, mais ne rend pas automatiquement la réponse plus petite ni moins coûteuse à calculer.
Les exports sont un cas particulier. Envoyer une réponse JSON de 200 Mo n'est pas équivalent à proposer un téléchargement. Un téléchargement peut être streamé comme une réponse de type fichier (CSV/JSONL) et consommé au fil de l'eau. Un énorme JSON API force souvent les clients à tout parser d'un coup, ce qui peut figer l'interface ou planter des applis mobiles.
Les limites sont le filet de sécurité. Elles empêchent les requêtes pire-cas d'abattre votre appli quand quelqu'un choisit « tout » et « toutes les périodes ». De bonnes limites incluent généralement un nombre maximal de lignes ou d'octets, un temps maximal de requête, et des quotas sur les endpoints d'export. Beaucoup d'équipes ajoutent aussi des valeurs par défaut raisonnables (plages de dates limitées) et exigent un job asynchrone pour les rapports très volumineux.
Les équipes qui réparent des exports générés par IA ont souvent besoin des trois : compression pour la vitesse, streaming pour la sécurité mémoire, et limites pour qu'une requête ne mette pas tous les autres en danger.
Choisir gzip ou brotli sans deviner
La compression consiste à emballer la réponse dans moins d'octets avant l'envoi. Le client la décompresse automatiquement. Pour de gros payloads JSON et les exports, cela peut faire la différence entre un téléchargement rapide et une requête qui timeoute.
Gzip est l'option plus ancienne et la plus compatible. Brotli est plus récent et réduit souvent un peu plus le texte, surtout JSON et HTML. Les deux fonctionnent mieux sur du texte. Aucun ne sert beaucoup si la réponse est déjà compressée.
Comment client et serveur décident
Les clients indiquent ce qu'ils peuvent décoder via l'en-tête de requête Accept-Encoding (par exemple : br, gzip). Le serveur choisit l'une de ces encodages et met Content-Encoding dans la réponse. Si l'en-tête est absent, envoyez la réponse non compressée.
Une règle pratique : choisissez le meilleur encodage que le client indique supporter, avec un fallback sûr.
- Si
brest accepté, utilisez Brotli pour les réponses textuelles. - Sinon, si
gzipest accepté, utilisez gzip. - Si aucun des deux n'est accepté, envoyez les octets bruts.
Quand gzip est plus sûr et quand brotli a du sens
Choisissez gzip par défaut quand vous avez besoin d'une compatibilité maximale (clients anciens, proxies inhabituels, environnements mixtes). Choisissez Brotli quand la majeure partie du trafic vient de navigateurs modernes ou de vos propres clients contrôlés, et que vous tenez à économiser un peu de bande passante.
Gardez en tête le coût CPU. Des réponses plus petites peuvent coûter plus de travail serveur. Brotli utilise souvent plus de CPU que gzip à paramètres similaires. Si votre serveur est déjà occupé à générer des rapports, la compression peut le pousser au-delà de ses limites. Une approche courante : gzip pour la plupart des JSON d'API, Brotli pour les endpoints orientés navigateur, et niveaux de compression faibles pour les très grands téléchargements.
Évitez aussi de compresser des formats déjà compressés (ZIP, PDF, PNG/JPEG, beaucoup de types audio/vidéo). Vous gaspillez du CPU et parfois le fichier devient même plus gros.
Si vous héritez d'un backend généré par IA, un bon chemin « stabiliser vite » est gzip pour les réponses textuelles plus des limites claires. Ajoutez Brotli seulement si vous pouvez prouver que ça aide.
Comment ajouter la compression en toute sécurité
La compression est souvent le gain le plus rapide, mais elle peut créer des bugs déroutants si les en-têtes sont faux ou si les données sont compressées deux fois.
Commencez par une règle simple : compressez uniquement quand ça aide. Si la réponse est petite, la compression peut gaspiller du CPU et ajouter de la latence. Un seuil pratique est autour de 1–2 Ko pour du JSON et 4–8 Ko pour du CSV ou du texte simple. En dessous, envoyez tel quel.
La compression marche mieux pour du contenu textuel comme JSON, CSV, HTML et logs. Elle fait peu pour des images, PDF ou fichiers déjà compressés. Pour ceux-là, ignorez la compression.
Mettez les en-têtes pour que navigateurs, proxies et caches se comportent bien :
Content-Encoding: gzipoubrpour indiquer ce que vous avez utiliséVary: Accept-Encodingpour que les caches ne mélangent pas versions compressées et non compressées- Un
Content-Typecorrect (par exempleapplication/jsonoutext/csv) pour que les clients parsèment correctement - Si vous streammez, évitez de définir un
Content-Lengthque vous ne pouvez pas garantir
Évitez la double compression. Cela arrive quand votre appli compresse les réponses et qu'un reverse proxy ou middleware compresse à nouveau. Choisissez un seul endroit pour le faire, puis vérifiez en contrôlant les en-têtes de réponse et en testant rapidement que les octets correspondent au Content-Encoding.
Testez comme un mini benchmark : comparez taille de réponse, temps total et CPU avant/après. Testez aussi sur réseaux lents et l'action « annuler en plein téléchargement », car une mauvaise mise en place de la compression se manifeste souvent par des téléchargements cassés ou des requêtes qui restent bloquées.
Streamer les exports pour ne pas exploser la mémoire
La façon la plus sûre de gérer les gros exports est de ne jamais construire le fichier entier en mémoire. Générez une ligne (ou un petit lot) et envoyez-la immédiatement au client. Bien fait, le serveur fait un travail constant et le téléchargement grossit au fil du temps.
Pour les exports, les formats de type fichier sont généralement plus simples qu'un « énorme tableau JSON » car vous pouvez écrire ligne par ligne. Le CSV fonctionne bien pour les tableurs. NDJSON (un objet JSON par ligne) est adapté au traitement machine et aux données de type logs.
Quand vous streammez de grandes réponses, les connexions lentes comptent. Si un utilisateur télécharge sur un lien mobile faible, le serveur ne doit pas bufferiser l'export entier en attendant d'envoyer. Utilisez une écriture aware du backpressure (la plupart des frameworks web la supportent) pour ne produire des données que aussi vite qu'elles peuvent être livrées.
Les téléchargements longs nécessitent aussi des timeouts amicaux. Gardez la connexion vivante avec des sorties périodiques, et réglez les timeouts serveur/proxy assez hauts pour les tailles de rapports attendues. Si vous avez un reverse proxy en frontal, confirmez qu'il autorise des réponses longues sinon il tronquera l'export en chemin.
Le streaming change la gestion des erreurs. Une fois que vous avez commencé à envoyer le fichier, vous ne pouvez pas retourner une belle réponse d'erreur JSON. Préparez-vous avant de déployer :
- Validez les entrées et les permissions avant d'envoyer le premier octet.
- Écrivez un en-tête tôt (colonnes CSV, ou une ligne de métadonnées pour NDJSON).
- Si quelque chose échoue en cours de stream, loggez-le, arrêtez proprement et rendez évident que le fichier est incomplet.
Un bug courant « marche en dev » consiste à construire des tableaux de centaines de milliers de lignes. Passer au streaming supprime généralement le pic mémoire immédiatement et garde l'appli réactive pendant le téléchargement.
Étape par étape : implémenter un téléchargement streaming sûr
Traitez un téléchargement comme un tuyau vivant, pas comme un gros objet que vous construisez en mémoire pour le renvoyer à la fin.
Commencez par choisir le format d'export selon l'usage. Le CSV est parfait pour les tableurs. NDJSON est meilleur quand un autre système doit le lire. Un bundle zippé peut aider si vous envoyez plusieurs fichiers, mais n'utilisez pas ZIP juste pour cacher des problèmes de performance.
Ensuite, rendez le travail incrémental. Plutôt qu'une grosse requête, lisez les lignes par pages (ou via un curseur) et bouclez jusqu'à épuisement. Votre appli ne doit garder qu'une petite tranche à la fois.
Une séquence simple qui évite la plupart des échecs « le rapport a tué le serveur » :
- Définissez les en-têtes dès le départ (type + nom de fichier pour le téléchargement) et commencez la réponse.
- Récupérez les données par pages et convertissez chaque page en lignes de sortie.
- Écrivez des chunks et flush souvent (ne construisez pas une énorme chaîne).
- Compressez à la volée quand c'est utile (gzip en streaming est largement supporté).
- Arrêtez le travail quand le client se déconnecte ou annule.
La compression est le bonus, pas la base. Elle réduit la bande passante, mais le streaming est ce qui maintient la mémoire stable. Pour CSV et NDJSON, gzip donne souvent un gros gain, tant que vous compressez en écrivant et non après avoir généré tout le fichier.
Validez avec un jeu de données réaliste. Un test à 1 000 lignes peut sembler parfait alors qu'un export à 5 millions de lignes vide la mémoire, timeoute ou produit un fichier tronqué.
Exemple : un export « Transactions mensuelles » échoue en prod parce qu'il charge toutes les lignes, puis les joint en une seule chaîne CSV. Passer à une boucle paginée plus des écritures par chunks le corrige sans changer l'expérience utilisateur.
Appliquer des limites de taille et de temps acceptables pour les utilisateurs
Pour accepter de grandes réponses sans plantages aléatoires, il faut des limites qui protègent le serveur et restent justes pour les utilisateurs. L'astuce est de rendre les limites prévisibles, visibles et accompagnées d'une solution évidente.
Commencez par deux plafonds stricts : lignes max et octets max. Les plafonds sur les lignes empêchent les requêtes lentes de tourner indéfiniment. Les plafonds en octets évitent des réponses « réussies » qui saturent les proxies ou buffers. Quand un export atteint une limite, renvoyez un message clair indiquant ce qui s'est passé et comment procéder (par exemple : « Export limité à 100 000 lignes. Réduisez la plage de dates ou ajoutez un filtre. »).
Mettez des garde-fous sur la requête elle-même pour réduire la charge en base. Des garde-fous courants : plage par défaut (31 ou 90 jours), exiger au moins un filtre réducteur pour les rapports « tous les clients », et une taille de page maximale même si le client en demande plus. Si vous autorisez tri ou filtres, gardez une allow-list et assurez-vous que la base peut les supporter.
Les limites de temps doivent exister à plusieurs niveaux : timeout de requête SQL, timeout de requête serveur, et un délai applicatif pour générer les exports. Quand vous coupez le travail, faites-le proprement. Retournez une erreur spécifique qui indique comment réessayer, pas un 500 générique.
Le rate limiting est l'autre moitié des limites « vivables ». Un utilisateur qui télécharge en boucle un gros rapport ne doit pas priver tout le monde. Throttlez les endpoints coûteux par utilisateur et par organisation, et pensez à des limites séparées pour les requêtes interactives vs les exports.
Enfin, loggez les requêtes proches des limites (lignes, octets, temps, filtres utilisés) et alertez quand elles se regroupent. Si beaucoup d'utilisateurs atteignent le plafond de 90 jours, c'est un signal pour ajouter un rapport récapitulatif ou une option d'export asynchrone.
Vérifications de sécurité pour les exports et grandes réponses
Les gros exports échouent de deux façons : ils plantent l'appli, ou ils fuient silencieusement des données. Traitez les exports comme une fonctionnalité séparée avec ses propres règles de sécurité.
Commencez par l'autorisation. Un bug fréquent est un endpoint d'export qui vérifie « l'utilisateur est connecté ? » mais oublie « a-t-il le droit de voir ces lignes ? ». Réutilisez les mêmes contrôles de permission que le rapport à l'écran, et appliquez-les côté serveur avant d'écrire quoi que ce soit.
Les exports CSV ont un risque particulier : l'injection CSV. Si un champ contrôlé par l'utilisateur commence par =, +, - ou @, l'ouverture du fichier dans un tableur peut exécuter une formule. La correction est simple : échapper ou préfixer les valeurs risquées (par exemple, ajouter une apostrophe en tête) pour les cellules exportées issues d'utilisateurs.
Les échecs d'export peuvent aussi répandre des secrets. Quand un job timeoute, il est tentant de logger la requête complète, les en-têtes ou le corps. Cela peut exposer des API keys, tokens d'auth ou des données personnelles dans les logs. Préférez un ID d'export interne plus un code d'erreur court, et gardez les valeurs sensibles hors des traces de pile.
Les filtres de rapport trop flexibles sont un autre piège. « Trier par n'importe quelle colonne » ou « filtrer avec une chaine query brute » peut conduire à de l'injection SQL si vous construisez du SQL par concaténation. Utilisez des requêtes paramétrées et des allow-lists pour les champs triables et filtrables.
Protégez aussi les exports longue durée contre les abus. Quelques garde-fous suffisent :
- Re-vérifier le token utilisateur (ou la session) quand l'export démarre, pas seulement quand il a été mis en file
- Limiter les exports par utilisateur/espace de travail
- Mettre un plafond strict sur les lignes ou le temps et renvoyer un message clair quand la limite est atteinte
- Enregistrer qui a exporté quoi et quand pour l'audit
Ces vérifications se manquent facilement quand l'équipe est concentrée sur « faire télécharger le fichier ». L'objectif est un téléchargement fiable et sûr en production.
Erreurs courantes qui provoquent des téléchargements cassés
Les téléchargements cassés arrivent généralement parce que le serveur veut être « utile » au mauvais endroit. Un test rapide avec de petites données semble correct, puis un vrai rapport en prod bloque l'appli, timeoute ou renvoie un fichier inutilisable.
Un piège facile est la compression partout. Compresser une réponse JSON de 2 Ko peut coûter plus de CPU que ça n'économise, surtout sous charge. La compression brille sur les réponses longues et répétitives (exports, longues listes, logs). Pour les petites réponses, ignorez-la ou imposez une taille minimale.
Une autre erreur fréquente est de construire tout l'export en mémoire avant de l'envoyer. C'est plus simple, mais ça ne scale pas. Un CSV de 200 Mo peut devenir bien plus gros en mémoire pendant le formatage, et quelques utilisateurs simultanés peuvent redémarrer le process.
Autres erreurs récurrentes :
- Appeler quelque chose « streaming » tout en générant d'abord le CSV complet puis en l'écrivant
- Streamer du JSON de façon à produire du JSON invalide (crochets manquants, virgules superflues, objets partiels)
- Ignorer les timeouts du client, du reverse proxy ou du load balancer (l'export tourne mais la connexion est déjà morte)
- Tester seulement sur un réseau local rapide avec de petites données, puis déployer sans test réseau lent ni test taille réelle
- Atteindre des limites (taille/temps) sans message clair, laissant l'utilisateur avec un téléchargement raté
Le streaming JSON mérite une attention particulière. Si vous avez besoin d'un JSON strict, streamez un tableau bien formé et gérez les virgules avec soin. Si les clients peuvent accepter autre chose, choisissez un format conçu pour le streaming comme JSON Lines/NDJSON.
Quand des limites sont atteintes, dites à l'utilisateur ce qui s'est passé et comment faire autrement (affiner les filtres, réduire la plage de dates, ou demander un export asynchrone).
Un exemple réaliste : réparer un export qui échoue
Un fondateur clique sur « Rapport des ventes mensuelles » et attend. L'onglet du navigateur tourne, l'appli devient lente pour tout le monde, et au bout d'une minute le téléchargement échoue. Côté serveur, l'endpoint build le CSV entier en mémoire avant d'envoyer quoi que ce soit. Un gros mois (ou quelques colonnes en plus) pousse la mémoire au-delà de la limite et le process redémarre.
La correction n'a pas été « augmenter le serveur ». C'était de changer la façon dont l'export est produit et envoyé pour qu'il supporte de grands jeux de données de façon prévisible.
Ce qui a changé :
- Le serveur écrit les lignes CSV au fur et à mesure qu'il les lit en base, au lieu de les collecter dans une énorme chaîne.
- La compression gzip est activée pour le téléchargement afin que le fichier soit plus léger sur le réseau.
- Un plafond strict est ajouté (par exemple 31 jours à la fois) avec un message clair si l'utilisateur en demande plus.
- Un timeout et une limite de lignes sont appliqués pour qu'une requête ne puisse pas monopoliser le système.
L'expérience utilisateur s'améliore immédiatement. Le téléchargement démarre en une ou deux secondes car le serveur peut envoyer les en-têtes et les premiers octets rapidement. Le fichier finit souvent plus vite car il est plus petit, et les échecs diminuent parce que le serveur ne tente plus de tout tenir en mémoire. Si l'utilisateur a besoin d'une plage plus longue, l'UI peut l'orienter vers plusieurs exports.
Pour l'équipe, le plus grand gain est la stabilité. La mémoire reste stable, les pics CPU sont plus faibles, et les tickets support du type « le rapport a figé l'appli » disparaissent. C'est le type de travail que fait souvent FixMyMess quand des prototypes générés par IA cassent en production : passer les exports en streaming, ajouter une compression sûre et appliquer des limites pour qu'un seul export ne puisse pas faire tomber l'appli.
Checklist rapide avant mise en production
Testez le pire cas, pas seulement le chemin heureux. Choisissez le plus gros rapport que vos utilisateurs peuvent raisonnablement demander et exécutez-le de bout en bout de la même façon (mêmes filtres, mêmes rôles, et si possible un appareil réel). C'est là que les téléchargements « marche sur ma machine » se cassent généralement.
Checklist :
- Lancez le plus gros export et confirmez qu'il se termine correctement (pas de 500, pas de fichiers partiels, pas d'erreur réseau après quelques minutes).
- Surveillez la mémoire serveur pendant l'export. Elle doit rester majoritairement plate. Une montée lente indique souvent du buffering au lieu de streaming.
- Affichez les limites là où l'utilisateur choisit le rapport : plage max, plafonds de lignes, et timeouts éventuels.
- Vérifiez l'autorisation avec de vrais rôles (admin, utilisateur standard, rôles restreints). Confirmez qu'on ne peut pas exporter des données non visibles.
- Vérifiez les logs après une grosse exécution : taille de réponse, compression utilisée, temps de génération, et si la requête a atteint une limite.
Si vous héritez d'un export généré par IA qui plante, la correction la plus rapide est souvent un audit court pour trouver où le buffering se produit, puis ajouter streaming et limites strictes.
Prochaines étapes si votre appli plante déjà
Si l'appli plante quand les utilisateurs lancent de gros rapports, traitez cela comme un incident : arrêtez l'hémorragie d'abord, puis améliorez l'expérience. Optimiser la compression avant d'avoir des garde-fous est souvent une perte de temps.
Un ordre de travail sensé :
- Ajoutez des limites strictes (lignes max, octets max, temps max) et renvoyez une erreur claire quand elles sont atteintes.
- Passez les exports en streaming pour que le serveur ne charge jamais tout le fichier en mémoire.
- Affinez la compression une fois les bases stabilisées, et seulement là où elle aide.
Une fois les limites en place, vous pouvez supporter de gros téléchargements sans faire tomber le process. Le changement clé est d'éviter le buffering : ne construisez pas le JSON/CSV complet dans un tableau ou une chaîne, et ne loggez pas les payloads complets sur erreur.
Faites un plan de test pire-cas avant de toucher la prod :
- Le plus gros rapport que les utilisateurs lancent réellement (ou la plus grande table en prod)
- Un client réseau lent (throttled) pendant le téléchargement
- Deux ou trois exports concurrents de différents utilisateurs
- Un téléchargement annulé en plein milieu
- Une requête qui atteint la limite (vérifiez le message et que le serveur reste sain)
Si votre codebase a été générée par des outils comme Lovable, Bolt, v0, Cursor ou Replit, ces bugs se cachent souvent dans quelques endroits : wrappers auth qui retryent indéfiniment, gestion d'erreur qui dump des réponses complètes dans les logs, et utilitaires qui appellent des méthodes type toString() ou json() trop tôt (forçant un buffering complet).
Une remédiation rapide ressemble souvent à : diagnostic (trouver où la mémoire monte et où le buffering se produit), correctifs ciblés (limites + streaming + erreurs plus sûres), vérification (tests de charge et d'intégrité des exports), et préparation au déploiement (timeouts, dimensionnement des workers et monitoring). Si vous voulez un second avis, FixMyMess fixmymess.ai peut réaliser un audit de code gratuit pour repérer les points de crash dans les exports, les failles de sécurité et les problèmes de performance, puis aider à livrer une correction fonctionnelle en 48–72 heures.