API web rapide : les erreurs qui coûtent cher
API web rapide : les erreurs qui coûtent cher
Sur le papier, une API paraît souvent “rapide” tant qu’elle est testée en local, avec peu de données et un seul client. En production, la réalité est différente : latence réseau, base de données qui grossit, pics de trafic, sérialisation JSON, appels externes, logs, sécurité, retries. Beaucoup d’API tiennent pendant les premiers jours, puis se dégradent dès les premiers usages réels. Le problème n’est pas toujours le langage ou le framework. Il vient surtout de décisions de conception prises trop tôt, sans mesurer leur coût.
Dans un contexte backend, les erreurs les plus chères ne se voient pas immédiatement. Elles apparaissent quand le produit commence à être utilisé “pour de vrai” : une page qui charge 30 ressources, un mobile sur réseau 4G instable, un batch qui lance 10 000 requêtes, un client qui réessaie automatiquement en cas d’échec. C’est là que les compromis techniques deviennent visibles.
Cet article passe en revue les erreurs les plus fréquentes qui rendent une API lente, fragile ou coûteuse à exploiter, avec des exemples concrets et des pistes pour corriger le tir sans sur-ingénierie.
Confondre rapidité locale et performance réelle
Une API qui répond en 20 ms en local n’est pas forcément une API rapide. En production, il faut compter la latence réseau, le TLS, le passage par un reverse proxy comme Nginx ou Caddy, l’éventuel équilibrage de charge, puis les accès aux services tiers. Un simple aller-retour HTTP entre un navigateur et un serveur peut facilement coûter 50 à 150 ms selon la zone géographique et la qualité du réseau. Sur mobile, c’est souvent plus.
L’erreur classique consiste à optimiser le code métier en oubliant les coûts périphériques. Une route qui fait trois appels SQL, un appel à Stripe, puis un enrichissement via une API interne peut sembler correcte si chaque étape est “raisonnable”. Mais si chaque dépendance ajoute 80 ms, on dépasse vite les 300 à 500 ms, sans même parler des cas dégradés.
La bonne approche consiste à mesurer la latence de bout en bout. Des outils comme OpenTelemetry, Grafana, Datadog ou New Relic permettent de visualiser où part réellement le temps. Sans cette visibilité, on corrige souvent le mauvais problème.
Multiplier les requêtes à la base sans s’en rendre compte
L’un des pièges les plus coûteux reste le problème de type N+1. Il apparaît souvent avec les ORM comme Doctrine, TypeORM, Sequelize ou Entity Framework. Exemple typique : une route récupère 100 commandes, puis charge le client et les lignes de commande dans une boucle. Résultat : 1 requête principale + 200 requêtes secondaires.
Avec 2 ms par requête sur une base peu chargée, le problème semble mineur. Avec une base distante, des index imparfaits et de la concurrence, la facture explose. Une route qui devrait répondre en 80 ms passe à 800 ms ou plus.
Quelques signaux d’alerte :
- Le temps de réponse augmente fortement avec le nombre d’objets retournés.
- Les logs SQL montrent des centaines de requêtes pour une seule requête HTTP.
- La base de données sature alors que le CPU applicatif reste faible.
Il faut privilégier les jointures maîtrisées, le préchargement explicite, les projections utiles et des index adaptés. Sur PostgreSQL, un EXPLAIN ANALYZE permet souvent de repérer immédiatement un scan coûteux ou un index manquant. C’est une habitude simple qui évite des semaines de dégradation progressive.
Retourner trop de données “au cas où”
Beaucoup d’API deviennent lentes parce qu’elles renvoient des réponses trop riches. Le raisonnement paraît pratique : “on renvoie tout, le front choisira”. En réalité, cette approche augmente la taille des payloads, le temps de sérialisation, la consommation mémoire et parfois le coût de parsing côté client.
Un JSON de 20 Ko n’est pas dramatique. Mais une liste paginée de 100 objets contenant chacun 40 champs, des relations imbriquées et des métadonnées répétées peut vite atteindre 500 Ko à 2 Mo. À cette taille, la compression gzip ou brotli aide, mais ne compense pas une mauvaise conception.
Exemple concret : sur une API e-commerce, une route /products qui renvoie description longue, galerie complète, stock détaillé, variantes, avis et recommandations pour chaque produit est rarement justifiée sur une page de listing. Dans la plupart des cas, il suffit de renvoyer l’identifiant, le nom, le prix, la disponibilité et une image.
Une API rapide est souvent une API sobre. Cela passe par :
- des champs limités selon l’usage ;
- une pagination réelle ;
- des relations chargées explicitement ;
- des formats de réponse stables et lisibles.
Ignorer la pagination ou la concevoir trop tard
Une route qui renvoie “tous les résultats” fonctionne toujours au début. Puis la table passe de 500 lignes à 500 000, et la route devient inutilisable. C’est une erreur fréquente sur les endpoints d’administration, les exports, les historiques d’événements ou les journaux d’activité.
La pagination ne doit pas être un patch. Elle fait partie du contrat d’API. Sur des données triées, une pagination par curseur est souvent plus robuste qu’un simple OFFSET/LIMIT, surtout quand les volumes augmentent. Avec un offset élevé, la base doit souvent parcourir beaucoup de lignes avant de renvoyer la page demandée.
Sur PostgreSQL, demander une page profonde avec OFFSET 100000 peut devenir sensiblement plus coûteux qu’une pagination basée sur un identifiant ou un timestamp. Pour les flux d’activité, les messages ou les événements, le curseur est généralement un meilleur compromis.
Faire des appels synchrones partout
Une autre erreur chère consiste à empiler les dépendances synchrones dans le chemin critique. Une requête utilisateur déclenche alors un appel à la base, un appel à un service de paiement, un appel à un moteur de recherche, un appel à un service d’email, puis un log externe. Tant que tout va bien, la route répond. Dès qu’un service ralentit, toute la chaîne ralentit.
C’est exactement le type d’architecture qui produit des incidents en cascade. Un timeout sur un service tiers peut monopoliser les workers, remplir les files d’attente, provoquer des retries et finir par dégrader tout le système.
Il faut distinguer ce qui est indispensable à la réponse immédiate de ce qui peut être traité en arrière-plan. L’envoi d’un email, l’indexation dans Elasticsearch, la génération d’un PDF ou la propagation d’un webhook peuvent souvent passer par une file comme RabbitMQ, Redis avec BullMQ, ou Amazon SQS.
Le gain n’est pas seulement la performance. C’est aussi la résilience.
Mal gérer les timeouts, retries et circuit breakers
Par défaut, beaucoup de clients HTTP sont trop permissifs. Certains n’imposent pas de timeout strict, ou laissent des délais trop élevés. Une dépendance lente peut alors bloquer des ressources pendant plusieurs secondes. Avec 100 requêtes simultanées, la saturation arrive vite.
En backend web, un timeout de 10 à 30 secondes sur un appel interne est souvent beaucoup trop haut pour un endpoint interactif. Selon le cas d’usage, 300 ms, 800 ms ou 2 secondes peuvent être des limites plus réalistes. L’important est d’avoir une stratégie claire.
Les retries automatiques posent aussi problème. Si un service en difficulté reçoit en plus une vague de nouvelles tentatives, on amplifie l’incident. C’est l’effet classique de tempête de retries. Des mécanismes de backoff exponentiel, de jitter et de circuit breaker sont utiles pour éviter cela. Des bibliothèques comme Resilience4j dans l’écosystème Java ou des middlewares équivalents côté Node.js aident à poser un cadre.
Négliger le cache, ou l’utiliser n’importe comment
Le cache est souvent traité comme une solution magique ou, à l’inverse, complètement ignoré. Dans les deux cas, on paie l’addition. Un bon cache réduit la charge et améliore la latence. Un mauvais cache crée des incohérences, des bugs métier et des invalidations impossibles à comprendre.
Les cas simples sont pourtant très rentables : catalogue produit, configuration, pages publiques, résultats peu volatils, réponses d’API tierces avec durée de vie connue. Un cache HTTP bien configuré avec ETag, Cache-Control et éventuellement un CDN comme Cloudflare peut éviter un grand nombre de requêtes inutiles.
Côté applicatif, Redis reste un standard. Mais il faut définir :
- la clé de cache ;
- la durée de vie ;
- la stratégie d’invalidation ;
- le comportement en cas de cache manquant ou obsolète.
Sans cela, on crée un système rapide… jusqu’au jour où les utilisateurs voient des données périmées ou contradictoires.
Sous-estimer le coût de la sérialisation et des transformations
Quand on parle performance, on pense souvent SQL ou réseau. Pourtant, la sérialisation JSON peut coûter cher, surtout avec de gros objets, des transformations multiples et des couches d’abstraction qui copient les données plusieurs fois.
Dans certains services Node.js ou PHP, il n’est pas rare de voir une part significative du temps CPU partir dans la normalisation, la conversion de dates, le mapping DTO, puis la sérialisation finale. Si la route renvoie des milliers d’éléments, le coût devient visible.
Il faut surveiller :
- les transformations inutiles ;
- les objets imbriqués trop profonds ;
- les conversions répétées ;
- les champs calculés coûteux exécutés à chaque élément.
Une réponse plus petite et plus directe est souvent plus efficace qu’une architecture de mapping très “propre” sur le papier mais lourde à l’exécution.
Oublier l’observabilité dès le départ
Une API lente sans métriques est presque impossible à corriger rapidement. Beaucoup d’équipes attendent l’incident pour ajouter des traces, des dashboards et des alertes. C’est trop tard. Dès les premiers usages réels, il faut suivre au minimum :
- le temps de réponse médian et en p95/p99 ;
- le taux d’erreur ;
- le nombre de requêtes par route ;
- le temps passé en base de données ;
- le temps passé dans les appels externes ;
- la saturation CPU, mémoire et pool de connexions.
Le p95 et le p99 sont souvent plus instructifs que la moyenne. Une moyenne à 120 ms peut masquer un p99 à 3 secondes, ce qui correspond à une vraie douleur utilisateur. Avec Prometheus et Grafana, il est possible d’obtenir une base solide sans stack disproportionnée.
Une API fiable n’est pas celle qui va vite une fois. C’est celle qui garde un comportement prévisible quand le trafic, les données et les dépendances augmentent.
Choisir la complexité trop tôt
À l’inverse, certaines équipes se tirent une balle dans le pied en adoptant trop tôt une architecture distribuée : microservices, bus d’événements, cache multicouche, CQRS, recherche externe, workers spécialisés. Chaque brique ajoute des coûts de latence, d’exploitation et de coordination.
Pour beaucoup de produits en démarrage, un monolithe bien structuré avec une base PostgreSQL, un cache Redis, une file simple et de bonnes métriques tient très bien la charge. GitHub, Basecamp ou Shopify ont largement montré qu’un système sobre et bien maîtrisé peut aller loin avant de se fragmenter.
Le vrai sujet n’est pas de faire “moderne”. C’est de conserver un système lisible, mesurable et réparable.
Ce qu’il faut mettre en place dès maintenant
Si une API doit rester rapide et fiable après ses premiers usages réels, quelques décisions ont un retour sur investissement immédiat :
- définir des objectifs de latence réalistes par endpoint ;
- mesurer le p50, p95 et p99 dès le début ;
- paginer systématiquement les listes ;
- éviter les payloads trop riches ;
- auditer les requêtes SQL et les index ;
- poser des timeouts stricts sur les appels externes ;
- sortir du chemin critique tout ce qui peut être asynchrone ;
- ajouter du cache seulement sur des cas bien compris ;
- tester avec des volumes de données réalistes, pas seulement en local.
Le plus important reste la discipline de mesure. Une API peut être écrite en Go, Node.js, PHP, Python ou Java et rester très performante si ses chemins critiques sont simples, ses dépendances maîtrisées et ses volumes anticipés. À l’inverse, même la stack la plus rapide devient coûteuse si elle accumule les mauvaises décisions de conception.
Sur Code Brut, l’enjeu n’est pas de chercher la performance de benchmark. C’est de construire des API qui tiennent dans le réel : trafic irrégulier, équipes réduites, dette technique inévitable et contraintes budgétaires concrètes. Les erreurs qui coûtent cher sont rarement spectaculaires. Ce sont souvent des choix banals, répétés, jamais remis en question. C’est pour cela qu’il faut les voir tôt.