Comment nous benchmarquons un backend : méthodologie complète et reproductible
Un chiffre de benchmark sans méthode ne prouve rien. « p95 sous X ms », « cold start inférieur à Y ms » — n’importe qui peut écrire ça sur une page marketing. Ce qui prouve quelque chose, c’est la méthode : le matériel utilisé, la durée du test, le protocole de mesure, et la possibilité pour un tiers de le reproduire à l’identique.
Cet article répond à une question précise — comment benchmarker une API backend de façon reproductible — en documentant le protocole que nous appliquerons chez Aurabase avant de publier le moindre chiffre de performance. Pas des résultats : une méthode. Toute mesure déjà publiée ailleurs sur ce site (notamment sur notre page Performances) qui ne s’appuie pas encore sur ce protocole doit être traitée comme non vérifiée jusqu’à nouvel ordre.
Aucun résultat de performance Aurabase suivant un protocole publié et reproductible n’existe à ce jour — cet article documente la méthodologie que nous appliquerons pour en produire, pas des résultats déjà obtenus. Le dépôt contient déjà une suite de test à 3 niveaux : micro-benchmarks Criterion.rs sur 3 crates, tests de charge k6 sur 8 scénarios HTTP/WebSocket, et un script Python de comparaison Postgres direct vs API avec calcul de percentiles. Le protocole complet — durée de mesure, percentiles plutôt que moyennes, isolation de l’environnement, disclosure de version et de date — s’appuie sur des sources externes vérifiées : PostgreSQL, Criterion.rs, k6 (Grafana), HdrHistogram, PlanetScale et Convex. Toute affirmation de performance déjà publiée ailleurs sur ce site sans être retracée à ce protocole doit être considérée comme non vérifiée.
Pourquoi nous ne publions pas de chiffres nus
Convex, éditeur d’un backend réactif concurrent, a pris ses distances publiquement avec ce que son équipe technique appelle la « guerre des bar charts » entre fournisseurs de bases de données. Sa formule est directe : « It’s scaling theater, not scaling » — du théâtre de passage à l’échelle, pas du passage à l’échelle réel (stack.convex.dev/on-competitive-benchmarks, blog technique Stack, consulté le 23 août 2026).
Son argument central : un benchmark qui compare deux systèmes aux garanties de cohérence, de topologie ou de modèle de tarification différents ne teste souvent pas la même chose, même quand il prétend le faire — « the benchmark is not actually testing the same thing ». Ce réflexe a un nom dans l’industrie : le benchmarketing, publier un chiffre choisi pour son effet marketing plutôt que pour sa rigueur méthodologique.
Notre réponse n’est pas de refuser de mesurer — refuser de publier un chiffre indéfiniment serait aussi malhonnête que d’en publier un non étayé. C’est de documenter d’abord comment nous mesurerions, avec quels outils et sous quelles conditions, avant de prétendre avoir mesuré quoi que ce soit. C’est aussi ce qui distingue une comparaison utile (comme notre comparatif Aurabase vs Appwrite, qui documente des différences d’architecture vérifiables) d’un comparatif de chiffres de performance sans protocole commun.
Ça compte particulièrement pour un lead technique ou un CTO qui doit défendre un choix de backend en comité technique : un chiffre qu’on ne peut pas retracer à une méthode ne survit pas à la première question un peu insistante. Un protocole documenté, lui, se défend — on peut montrer le script, la version testée, et refaire tourner le test devant quelqu’un si nécessaire.
Ce qui rend la plupart des benchmarks backend trompeurs
Deux pièges reviennent systématiquement : comparer des topologies différentes sans le signaler, et mesurer la latence d’une façon qui masque exactement les pauses qui comptent le plus pour l’utilisateur.
Sur le premier point, PlanetScale documente explicitement sa contrainte de parité matérielle : chaque environnement comparé doit tourner sur des ressources de calcul (vCPU, RAM) égales ou supérieures à l’instance de référence, dans la même région cloud (planetscale.com/benchmarks, méthodologie « Telescope », consultée le 23 août 2026). Sans cette discipline, un écart de latence peut simplement refléter une machine plus grosse — pas une architecture plus rapide.
Le même principe s’applique à l’état du cache et à la topologie réseau. Une instance qui vient de démarrer (cache Postgres froid, pool de connexions vide, plan de requête pas encore mis en cache) répond structurellement plus lentement qu’une instance qui tourne depuis une heure sous charge stable. Une requête depuis la même région que la base de données répond structurellement plus vite qu’une requête inter-régions. Deux benchmarks qui ne précisent ni l’un ni l’autre ne sont tout simplement pas comparables, même s’ils affichent des unités identiques.
Sur le second point, le piège s’appelle la coordinated omission. HdrHistogram, le projet de référence sur la mesure de latence créé par Gil Tene, l’explique ainsi : quand un générateur de charge attend la réponse d’une requête avant d’envoyer la suivante (boucle fermée), une pause de service fait mécaniquement chuter le nombre de requêtes envoyées pendant la pause — et donc le nombre de mesures de latence élevée enregistrées (github.com/HdrHistogram/HdrHistogram, consulté le 23 août 2026). Le projet donne un exemple concret et chiffré : sur un système hypothétique qui échantillonne sa latence toutes les 10 ms pendant 200 secondes, une seule pause de 100 secondes au milieu du test suffit à produire, sans correction, un histogramme où environ 99,99 % des réponses semblent tenir sous 1 ms — alors que la moitié du temps réel s’est écoulée dans cette unique pause.
Pourquoi la moyenne ment : p50, p95, p99
Une moyenne de latence peut sembler excellente alors qu’une requête sur vingt met cinq fois plus de temps. C’est précisément ce que les percentiles révèlent et que la moyenne dissimule structurellement.
Mécaniquement, un percentile n’a rien de mystérieux : triez toutes les latences mesurées par ordre croissant, puis prenez la valeur à la position correspondante. Sur 1000 requêtes triées, le p50 est la 500e valeur, le p95 la 950e, le p99 la 990e. Une seule requête anormalement lente parmi 1000 suffit à faire bouger le p99 — c’est justement sa sensibilité aux cas rares qui le rend utile, là où cette même requête isolée n’a presque aucun effet sur la moyenne.
Signe révélateur : le rapport texte que pgbench — l’outil de benchmark officiel de PostgreSQL — affiche par défaut donne une moyenne et un écart-type, pas des percentiles (postgresql.org/docs/current/pgbench.html, consulté le 23 août 2026). Sa documentation officielle prévient d’ailleurs : « Never believe any test that runs for only a few seconds » — ne croyez jamais un test qui ne tourne que quelques secondes, ce qui vaut autant pour la durée que pour la métrique choisie.
k6, l’outil de charge que nous utilisons pour le niveau 2 de notre suite, résout ça avec des seuils exprimés en percentile : la syntaxe p(95)<500 définit un critère pass/fail — 95 % des requêtes doivent répondre sous 500 ms — directement dans la configuration du test (grafana.com/docs/k6, consulté le 23 août 2026).
Les 3 niveaux de benchmark déjà présents dans notre dépôt
Publier une méthodologie sans outillage réel serait juste une autre forme de théâtre. Le dossier benchmarks/ du dépôt Aurabase contient déjà une suite à 3 niveaux, inspirée dans sa structure de la méthodologie publique de Supabase — l’outillage existe, les résultats mesurés et datés, eux, n’existent pas encore.
Niveau 1 — Micro-benchmarks Criterion.rs
Trois crates du workspace Cargo ont des benchmarks CPU-bound dédiés : aura-crypto (hachage Argon2, JWT HS256 — génération, validation et signature pour PostgREST, chiffrement AES-GCM), aura-db-adapters (parsing de filtres et de select au format PostgREST — eq., gte., in.(), embeds de relations), et aura-core (sérialisation JSON, résolution de schema_name, validation d’UUID).
aura-db-adapters mesure spécifiquement le coût du parsing de requêtes au format PostgREST — quatre cas pour les filtres (simple_4, complex_10, or_group, in_large_50 avec 50 valeurs) et quatre pour le select (colonnes simples, *, un embed de relation, cinq embeds). C’est le genre de coût invisible dans un test de charge global : une regression sur le parsing d’un filtre or.(...) complexe ne changerait presque rien au p95 d’un endpoint peu utilisé, mais deviendrait mesurable sur un endpoint à fort trafic — d’où l’intérêt de l’isoler en micro-benchmark plutôt que de compter uniquement sur le niveau 2.
aura-core prend une approche différente : plutôt que de mesurer un temps brut, il mesure un débit (Throughput::Bytes) sur la sérialisation et la désérialisation JSON des messages internes NatsRequest/NatsResponse échangés entre la gateway et les services — avec trois tailles de charge utile réalistes (une requête minimale, une requête avec un corps JSON imbriqué, une réponse de liste à 50 lignes).
Criterion.rs ne se contente pas de chronométrer une boucle. Il exécute d’abord une phase de préchauffe pour remplir les caches CPU/OS, détecte les valeurs aberrantes avec une version modifiée de la méthode de Tukey (sans les exclure du jeu de données), calcule des intervalles de confiance par bootstrap sur un grand nombre d’échantillons rééchantillonnés, et détecte les régressions de performance entre deux runs par test statistique de Student, avec un seuil de bruit configurable — typiquement ±1 % — pour ignorer les variations qui ne sont pas statistiquement significatives (bheisler.github.io/criterion.rs/book/analysis.html, consulté le 23 août 2026).
target/criterion/ — distributions, graphiques de régression, comparaison au run précédent. C’est ce rapport, pas juste une ligne de terminal, qu’une méthodologie sérieuse doit permettre de régénérer.Niveau 2 — Tests de charge k6
Huit scripts k6 couvrent la gateway côté data plane : health (baseline de latence), auth-flow (register → login → refresh → logout), crud-read et crud-write, storage (upload/download), realtime-ws, breakpoint (montée en charge jusqu’à rupture) et supabase-compare. Sept sont câblés à une cible Makefile dédiée — supabase-compare.js existe dans le dépôt mais n’a pas encore de cible, un état de chantier que cet article documente tel quel plutôt que de le maquiller.
La configuration partagée définit des seuils par type d’opération. Ce sont des critères pass/fail que le test vérifie à chaque exécution — pas des résultats déjà mesurés :
README.md du dossier benchmarks/ documente un seuil de p95 en lecture à < 200ms (« Supabase SLO »), alors que le seuil réellement appliqué dans benchmarks/k6/lib/config.js — celui que le test exécute — est p(95)<500. Les deux fichiers ont dérivé l’un de l’autre. C’est un exemple concret, trouvé en lisant le code source pour cet article, de pourquoi un protocole doit avoir une seule source de vérité versionnée plutôt que d’être documenté à deux endroits : sans ça, même une équipe qui essaie d’être rigoureuse finit par publier des seuils contradictoires.Niveau 3 — Comparaison PostgreSQL direct vs API
Un script Python (direct_vs_api.py) mesure l’overhead réel de la couche gateway + service en comparant des requêtes psycopg2 directes à des appels HTTP sur la même opération — liste, lecture ponctuelle par id, lecture filtrée et triée. Chaque mesure suit un préchauffe de 10 itérations avant la boucle chronométrée, puis calcule moyenne, p50, p95, p99 et un débit en opérations par seconde.
Un second script (aurabase_vs_supabase.py) applique la même logique de warmup et de calcul de percentiles à une comparaison tête-à-tête avec une instance Supabase locale (CLI Supabase, localhost:54321 par défaut) — même machine, même réseau local pour les deux, exactement la discipline de parité d’environnement que PlanetScale documente pour ses propres comparatifs.
Un script d’orchestration (collect_baseline.sh, cible bench-baseline du Makefile) enchaîne les trois niveaux — Criterion sur les 3 crates, un sous-ensemble des scénarios k6 (health et crud-read aujourd’hui, pas encore l’ensemble des 8), puis la comparaison Python — et écrit logs, JSON et rapports HTML Criterion dans un dossier horodaté unique : benchmarks/results/AAAAMMJJ_HHMMSS/. C’est exactement le réflexe de disclosure daté, en un seul run reproductible, que la section suivante formalise en protocole complet.
Le protocole que nous appliquerons avant de publier un chiffre
Huit engagements, chacun ancré dans une pratique déjà documentée par un outil ou un projet tiers reconnu — pas inventés pour l’occasion.
- Préchauffe séparée de la mesure. Criterion.rs remplit les caches CPU/OS avant de chronométrer ;
pgbenchrecommande explicitement de ne jamais croire un run de quelques secondes seulement. - Durée fixe, pas un nombre d’itérations fixe. Une charge a besoin de temps pour converger — c’est le rôle des
stagesk6 et du flag-Tdepgbench. - Percentiles, jamais seulement la moyenne — et vigilance active sur la coordinated omission si le générateur de charge fonctionne en boucle fermée.
- Environnement documenté dans le détail : commit git du service testé, version de PostgreSQL, spécification matérielle, version de l’outil de charge. PlanetScale documente ses paramètres TPCC exacts (
TABLES=20,SCALE=250, ~500 Go) précisément pour cette raison — sans ces détails, personne ne peut reproduire un run. - Résultats horodatés et versionnés, jamais un chiffre unique gravé sur une page marketing sans date. Le tooling actuel écrit déjà dans un dossier daté ; il faudra étendre ce réflexe à toute mesure publiée publiquement, avec la région d’hébergement documentée comme n’importe quelle autre variable d’environnement (voir notre guide sur la souveraineté d’hébergement UE, pertinent dès qu’un chiffre dépend d’une région donnée).
- Scripts et données brutes publiés à côté du résultat agrégé, pas seulement une moyenne finale. PlanetScale invite même les lecteurs à signaler une erreur de méthodologie sur une adresse dédiée — une posture qu’on trouve saine et qu’on veut reprendre.
- Débit annoncé à côté de la latence, pas seulement l’un ou l’autre. Un système peut avoir une excellente latence à faible charge et s’effondrer en débit dès que la concurrence augmente — c’est exactement ce que le scénario
breakpointde notre suite k6 (montée en charge jusqu’à rupture) est conçu pour révéler, et ce que la mesure enThroughput::Bytesdes micro-benchmarks Criterion capture au niveau fonction. - Écart significatif avant d’annoncer une amélioration. Une variation de quelques pourcents entre deux runs peut être du bruit de mesure plutôt qu’un vrai gain — Criterion.rs calcule justement une probabilité que l’écart observé soit dû au hasard avant de le qualifier de régression ou d’amélioration. Un chiffre isolé, sans cette vérification, n’est qu’une anecdote statistique.
Ce que nous ne ferons pas
Cette liste compte autant que le protocole positif ci-dessus.
- Comparer des topologies différentes (auto-hébergé vs managé, instance à froid vs préchauffée) sans le signaler explicitement.
- Retenir le meilleur run sur dix sans mentionner les neuf autres.
- Publier un chiffre sans date, sans version de service, sans script de reproduction.
- Republier un chiffre marketing existant tant qu’il n’est pas retracé à ce protocole.
- Nous comparer à un concurrent sur un chiffre de performance brut si ce concurrent ne publie pas sa propre méthodologie de façon équivalente — un chiffre contre un silence n’est pas une comparaison, c’est un slogan.
Le protocole minimal pour benchmarker n’importe quel backend
Ce protocole ne dépend d’aucun outil Aurabase spécifique — vous pouvez l’appliquer à votre propre API dès aujourd’hui.
- Définissez la charge avant l’outil : lecture seule, écriture, mix réaliste pour votre application — pas un ratio générique copié d’un autre projet.
- Séparez explicitement la phase de préchauffe de la phase de mesure.
- Faites tourner le test assez longtemps — des minutes, pas quelques secondes.
- Mesurez en percentiles (p50/p95/p99), jamais en moyenne seule.
- Vérifiez que votre générateur de charge n’est pas en boucle fermée, ou corrigez la coordinated omission dans l’analyse.
- Isolez l’environnement testé — pas de voisin bruyant, pas de tâche de fond concurrente.
- Publiez la version testée, la date, la spécification matérielle et le script — pas seulement le résultat final.
Sur une base Postgres nue, ce protocole tient en une commande pgbench — 20 clients concurrents répartis sur 4 threads, pendant 5 minutes, avec un rapport de progression toutes les 10 secondes :
Outils de référence, par niveau
Cinq outils, chacun adapté à un niveau différent de la pile — aucun ne remplace les autres.