Retour au blog
Performances & Benchmarks · 15 min de lecture

Comment nous benchmarquons un backend : méthodologie complète et reproductible

Affane Daylami · Fondateur· 24 août 2026

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.

L’essentiel

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.

#
Posture éditoriale

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.

#
Diagnostic

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.

Piège classique
Un test de charge en boucle fermée qui n’envoie une requête qu’après avoir reçu la réponse précédente sous-représente systématiquement les pauses longues. La p99 qu’il affiche peut être meilleure que la réalité vécue par un utilisateur réel — pas parce que le système est rapide, mais parce que le protocole de mesure a « oublié » d’envoyer les requêtes pendant la pause.
#
Statistique

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).

p50 (médiane)La moitié des requêtes sont plus rapides que cette valeurMasque totalement la queue de distribution
p951 requête sur 20 est plus lenteZone où les premiers utilisateurs mécontents apparaissent
p991 requête sur 100 est plus lenteLa plus sensible à la coordinated omission si le protocole est mal conçu
#
Vérifié dans le code

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.

3
NIVEAUX DE TEST
Micro, charge HTTP, comparaison
3
CRATES BENCHMARKÉES
aura-crypto, aura-db-adapters, aura-core
8
SCRIPTS K6
7 câblés au Makefile, 1 en attente

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).

libs/aura-crypto/benches/crypto_bench.rs
RUST
// Extrait réel du dépôt
let mut group = c.benchmark_group("jwt/hs256");
group.bench_function("generate", |b| {
b.iter(|| jwt::generate_access_token(
black_box(user_id), black_box(project_id),
black_box("authenticated"), black_box(SECRET),
))
});
group.bench_function("validate", |b| {
b.iter(|| jwt::validate_token(black_box(&token), black_box(SECRET)))
});

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).

libs/aura-core/benches/core_bench.rs
RUST
group.throughput(Throughput::Bytes(
serde_json::to_vec(&small).unwrap().len() as u64
));
group.bench_function("NatsRequest/small", |b| {
b.iter(|| serde_json::to_vec(black_box(&small)).unwrap())
});

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).

Reproductible localement
Chaque run Criterion génère un rapport HTML détaillé dans 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.

benchmarks/k6/scenarios/crud-read.js
JAVASCRIPT
export const options = {
scenarios: {
crud_read: {
executor: 'ramping-vus',
startVUs: 10,
stages: [
{ duration: '30s', target: 50 },
{ duration: '1m', target: 200 },
{ duration: '30s', target: 0 },
],
},
},
thresholds: THRESHOLDS_READ,
}

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 :

Lecture (GET)p95 < 500 ms · p99 < 1000 msConfig k6 (benchmarks/k6/lib/config.js)
Écriture (POST/PATCH)p95 < 300 ms · p99 < 1000 msConfig k6
Auth (login/refresh)p95 < 300 ms · p99 < 1000 msConfig k6
Storage (upload/download)p95 < 500 ms · p99 < 2000 msConfig k6
Taux d’erreur, tous scénarios< 1 %Config k6
Une incohérence trouvée en écrivant cet article
Le 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.

benchmarks/comparison/direct_vs_api.py
PYTHON
def percentile(data, p):
k = (len(data) - 1) * (p / 100)
f = int(k)
c = f + 1
if c >= len(data):
return data[f]
return data[f] + (k - f) * (data[c] - data[f])

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.

#
Méthodologie

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.

  1. Préchauffe séparée de la mesure. Criterion.rs remplit les caches CPU/OS avant de chronométrer ; pgbench recommande explicitement de ne jamais croire un run de quelques secondes seulement.
  2. Durée fixe, pas un nombre d’itérations fixe. Une charge a besoin de temps pour converger — c’est le rôle des stages k6 et du flag -T de pgbench.
  3. Percentiles, jamais seulement la moyenne — et vigilance active sur la coordinated omission si le générateur de charge fonctionne en boucle fermée.
  4. 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.
  5. 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).
  6. 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.
  7. 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 breakpoint de notre suite k6 (montée en charge jusqu’à rupture) est conçu pour révéler, et ce que la mesure en Throughput::Bytes des micro-benchmarks Criterion capture au niveau fonction.
  8. É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.
#
Engagement éditorial

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.
Un exemple concret, déjà corrigé en interne
Un chiffre comme « cold start inférieur à 1 ms » a circulé sans être adossé à un benchmark reproductible. Il est aujourd’hui traité en interne comme non étayé et ne doit pas être lu comme une caractéristique mesurée du produit tant qu’aucune mesure datée, avec méthodologie publiée, ne le confirme. C’est précisément le genre d’affirmation que ce protocole existe pour empêcher de reproduire.
#
Reproductible

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.

  1. 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.
  2. Séparez explicitement la phase de préchauffe de la phase de mesure.
  3. Faites tourner le test assez longtemps — des minutes, pas quelques secondes.
  4. Mesurez en percentiles (p50/p95/p99), jamais en moyenne seule.
  5. Vérifiez que votre générateur de charge n’est pas en boucle fermée, ou corrigez la coordinated omission dans l’analyse.
  6. Isolez l’environnement testé — pas de voisin bruyant, pas de tâche de fond concurrente.
  7. 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 :

terminal
BASH
# Initialiser le jeu de données de test (scale factor >= nombre de clients)
pgbench -i -s 50 ma_base
# -c clients concurrents, -j threads, -T durée en secondes, -P intervalle de rapport
pgbench -c 20 -j 4 -T 300 -P 10 ma_base
#
Outillage

Outils de référence, par niveau

Cinq outils, chacun adapté à un niveau différent de la pile — aucun ne remplace les autres.

Micro (fonction)Criterion.rsCPU pur, statistiques bootstrap
Requête SQLpgbenchTransaction TPC-B-like, tps et latence
Charge HTTP/WSk6 (Grafana)Percentiles, seuils pass/fail
OLTP à l’échellesysbench + TPCC (méthodologie Telescope)QPS, coût par performance
Correction de mesureHdrHistogramCompense la coordinated omission
#
Questions Fréquentes

FAQ

Pourquoi Aurabase ne publie-t-il pas encore de chiffres de benchmark ?+
Parce qu’aucun chiffre de performance n’a été mesuré selon un protocole publié et reproductible à ce jour. Un chiffre comme « cold start inférieur à 1 ms », par exemple, a circulé sans être adossé à un benchmark reproductible : il est aujourd’hui traité comme non étayé et ne doit pas être lu comme une caractéristique mesurée du produit. Cet article documente le protocole que nous suivrons avant de publier un résultat, précisément pour éviter de reproduire ce genre d’affirmation.
Qu’est-ce qu’un percentile p95 ou p99, et pourquoi pas la moyenne ?+
Le p95 est le temps de réponse en dessous duquel se trouvent 95 % des requêtes — 1 requête sur 20 est donc plus lente. Le p99 pousse ce seuil à 1 requête sur 100. La moyenne masque ces requêtes lentes parce qu’elle les dilue dans la masse des requêtes rapides ; les percentiles isolent la queue de distribution que les utilisateurs remarquent réellement.
Qu’est-ce que la « coordinated omission » ?+
C’est un biais de mesure décrit par le projet HdrHistogram : quand un outil de charge attend la réponse d’une requête avant d’envoyer la suivante (boucle fermée), une pause de service réduit mécaniquement le nombre de requêtes lentes enregistrées pendant cette pause. Le résultat final peut afficher une latence bien meilleure que ce qu’un utilisateur réel a vécu.
Peut-on reproduire ces tests soi-même ?+
Le protocole décrit ici — percentiles, préchauffe séparée, environnement documenté, résultats datés — est applicable à n’importe quelle API, avec des outils publics (k6, pgbench, Criterion.rs, sysbench). L’outillage interne Aurabase (le dossier benchmarks/ du dépôt) sert aujourd’hui au développement et n’est pas encore packagé comme une suite publique à un clic. Créez un projet pour tester votre propre charge sur l’API Aurabase avec vos propres scripts k6.
Quelle différence entre un percentile mesuré et un seuil SLA ?+
Un percentile (p95, p99) est une statistique calculée après coup sur des mesures réelles. Un seuil SLA (ou un seuil k6 comme p(95)<500) est une cible fixée à l’avance, que le test vérifie en pass/fail. Confondre les deux mène à présenter un objectif non atteint comme un résultat obtenu — c’est précisément la distinction que ce protocole impose de garder explicite à chaque chiffre publié.
Pourquoi mesurer le débit (throughput) en plus de la latence ?+
Un système peut répondre vite à faible charge et voir sa latence se dégrader brutalement une fois un seuil de concurrence dépassé — la latence seule ne montre pas où se situe ce seuil. Mesurer le débit (requêtes ou octets par seconde) en parallèle de la latence révèle ce point de bascule, ce que le scénario breakpoint de notre suite k6 est spécifiquement conçu pour trouver.
MÉTHODOLOGIE OUVERTE

Testez votre propre charge sur un projet Aurabase.

Créez un projet gratuit et pointez vos propres scripts k6 ou pgbench dessus — le protocole décrit ici s’applique à n’importe quelle API REST sur Postgres.

Aucune carte bancaire requise · 500 MB gratuits · 50 000 MAU