PostgREST : benchmark et limites réelles en production
PostgREST lui-même n'est presque jamais le goulot d'étranglement. Sur les instances dédiées d'Aurabase, une réplique tourne avec 50 à 250 millicores de CPU et 64 à 128 Mo de RAM. C'est un binaire Haskell léger qui traduit des requêtes HTTP en SQL, rien de plus. Les vraies limites qui apparaissent en production sont ailleurs. Quatre d'entre elles reviennent le plus souvent : le budget de connexions Postgres que consomment ses réplicas, et le coût d'un COUNT exact sous MVCC. Une troncature de réponse peut aussi rester invisible dans les en-têtes, tout comme une fenêtre de latence après chaque migration de schéma.
Notre article sur la compatibilité PostgREST détaille ce que le serveur couvre fonctionnellement (filtres, embedding, RPC, RLS) et ce qu'il laisse à votre charge. Celui-ci part d'ailleurs. Il documente, avec le code Aurabase et la documentation officielle de PostgREST comme sources, où et pourquoi PostgREST plafonne réellement à l'échelle. Nous ne reproduisons pas ici un banc de charge que nous n'avons pas fait tourner nous-mêmes. Notre méthodologie de benchmark explique pourquoi un chiffre isolé, sans protocole publié, ne nous semble pas fiable.
- PostgREST lui-même est léger : 50 à 250 millicores de CPU, 64 à 128 Mo de RAM par réplique sur les instances dédiées Aurabase. Le débit HTTP brut n'est presque jamais le facteur limitant en production.
- Le vrai plafond, c'est le budget de connexions Postgres :
PGRST_DB_POOL× réplicas. Vérifié dans le code Aurabase : 20 connexions par projet sur le palier dédié (10×2), 4 sur le palier mutualisé (2×2). C'est un choix délibéré pour faire tenir plus de tenants sur le mêmemax_connections. Prefer: count=exactforce un scan MVCC coûteux sur les grandes tables. PostgREST documente deux alternatives moins chères :count=plannedetcount=estimated, au prix d'un total approximatif.- Un plafond
db-max-rows(1000 lignes par défaut chez Aurabase) tronque une réponse SANS le signaler dansContent-Range(mesuré en conditions réelles, détaillé plus bas). - Après une migration DDL, le cache de schéma de PostgREST se recharge de façon asynchrone. Le gateway Aurabase réessaie jusqu'à 8 fois (environ 3,5 secondes cumulées au pire cas) avant d'abandonner, un comportement documenté directement dans le code.
Ce qu'un benchmark PostgREST mesure, et ce qu'il ne mesure pas
Un test de débit HTTP sur PostgREST mesure surtout Postgres, rarement PostgREST. Le serveur est une fine couche de traduction devant la base. Dans l'immense majorité des charges réelles, le temps de réponse est dominé par la requête SQL exécutée, pas par le processus qui l'a générée.
Le projet PostgREST maintient un dépôt dédié à ce sujet, PostgREST/postgrest-benchmark sur GitHub, qui suit les variations de débit d'une version à l'autre plutôt que de publier un chiffre marketing isolé. Nous ne l'avons ni exécuté ni republié ici. Ses résultats dépendent du matériel, de la taille du schéma et du scénario testé, exactement les variables que notre propre protocole de benchmark exige de documenter avant de citer un chiffre.
En dessous de PostgREST, c'est pgbench qui mesure la couche qui compte vraiment : le temps de transaction SQL sous charge concurrente. C'est l'outil de référence officiel de PostgreSQL (postgresql.org/docs/current/pgbench.html, consulté le 24 août 2026). Plutôt que reproduire ce protocole ici, cet article documente quatre limites architecturales concrètes de PostgREST en production, chacune vérifiée dans le code source d'Aurabase ou dans la documentation officielle du projet.
L'empreinte réelle d'une instance PostgREST chez Aurabase
Chaque projet Aurabase moteur Postgres reçoit deux réplicas PostgREST dédiés, colocalisés avec son cluster. Le manifeste Kubernetes qui les déploie fixe des ressources modestes.
Ce que ces réplicas consomment vraiment, ce n'est pas du CPU : ce sont des connexions sur le primaire Postgres. Chaque instance PostgREST se connecte en direct au primaire (-rw), sans passer par le pooler PgBouncer déployé pour le tenant. Ce choix est déjà détaillé dans notre article sur la compatibilité PostgREST : le mécanisme LISTEN/NOTIFY de rechargement de schéma exige une connexion persistante, incompatible avec un pooler en mode transaction. Ce que cet article ajoute : combien ça coûte concrètement, en connexions, et où ça plafonne.
La taille de ce pool par réplica (PGRST_DB_POOL) diffère volontairement selon le palier du projet, vérifié dans k8s_tenant.rs, la fonction qui construit le manifeste PostgREST de chaque projet :
Sur le palier dédié, la contrainte se relâche : un projet a son propre cluster CNPG, donc son propre max_connections, sans voisin à ménager. Sur le palier mutualisé, plusieurs projets d'une même organisation partagent un seul cluster : c'est ce contexte qui rend le budget de connexions déterminant, développé dans la section suivante.
Le budget de connexions décide combien de tenants tournent en même temps
Sur un cluster Postgres mutualisé, ce n'est pas le débit HTTP qui plafonne le nombre de projets actifs simultanément. C'est le nombre de connexions que leurs instances PostgREST tiennent ouvertes sur le primaire, comparé au max_connections disponible.
Aurabase dérive ce budget directement des limites réelles du cluster, vérifié dans fleet.rs : (max_connections − réserve superuser/CNPG − connexions réservées au pooler) ÷ connexions par projet éveillé, plancher à 1. La réserve fixe est de 10 connexions (superuser, instance manager CNPG, exporteur de métriques, marge admin du provisionneur). Sur les défauts livrés (pool à 2 par réplica, 2 réplicas, soit 4 connexions par projet éveillé), le calcul donne trois budgets différents selon le palier de dimensionnement du cluster.
Source : dérivé de fleet.rs::derive_wake_budget et wake_budget_for_org_plan, code Aurabase, relu le 24 août 2026.
Ce budget n'est pas un quota de projets possédés : une organisation team peut détenir 50 projets dont la plupart dorment. C'est un plafond de concurrence : le nombre de projets qui peuvent tenir des connexions ouvertes en même temps sur le primaire. Un réveil au-delà du budget n'échoue pas, il est différé jusqu'à ce qu'un projet frère se rendorme, vérifié dans le même fichier. Le sujet du dimensionnement de max_connections lui-même est développé dans notre article sur le tuning de max_connections, et le compromis dédié/mutualisé dans son ensemble dans base dédiée vs mutualisée.
Pourquoi Prefer: count=exact ralentit une requête sur une grande table
Demander un total exact force Postgres à compter les lignes visibles du résultat filtré à chaque requête, un coût qui grandit avec la table, pas une opération gratuite.
PostgreSQL ne maintient aucun compteur de lignes indexé prêt à l'emploi. Sous MVCC, la visibilité d'une ligne dépend de la transaction qui la lit. Un COUNT(*) exact doit donc visiter les lignes candidates plutôt que lire une valeur précalculée. C'est une limite structurelle bien documentée dans l'écosystème Postgres, y compris chez des éditeurs de bases analytiques comme ClickHouse, qui comparent leurs propres compteurs approximatifs au comportement transactionnel de Postgres.
PostgREST documente ces trois stratégies de comptage nativement (postgrest.org, consulté le 24 août 2026). La stratégie exact garantit un total au prix du scan. planned renvoie une estimation quasi gratuite issue du planificateur de requêtes. estimated bascule automatiquement entre les deux selon un seuil. Le choix n'est pas cosmétique : une pagination qui demande count=exact sur une table de plusieurs millions de lignes paie ce scan à chaque page, même quand l'utilisateur ne consulte jamais la dernière.
La troncature de db-max-rows est invisible sans count=exact
Un plafond de lignes peut tronquer une réponse PostgREST sans qu'aucun signal ne l'indique dans le corps ou les en-têtes, sauf à demander explicitement un total exact. Nous l'avons mesuré en conditions réelles sur une instance Aurabase dédiée, pas supposé.
Sur une table de test à 10 lignes avec PGRST_DB_MAX_ROWS=5, PostgREST v12.2.3 rend exactement le même en-tête Content-Range pour deux situations pourtant très différentes :
Sans count=exact, la réponse de 5 lignes est indiscernable d'une table qui n'en contiendrait réellement que 5 : Content-Range: 0-4/* décrit les lignes rendues, jamais la limite appliquée. Le plafond réellement en vigueur n'apparaît nulle part dans ce cas, mesuré directement sur le chemin PostgREST du SDK Aurabase.
db-max-rows (Aurabase le pose par défaut à 1000), un client qui compare data.length à la limite demandée pour détecter une page pleine peut se tromper. L'erreur apparaît dès que le plafond du serveur est plus bas que cette limite. Le seul signal fiable est de comparer le nombre de lignes reçues au total renvoyé par count=exact, ce qui remet directement en jeu le compromis de coût décrit à la section précédente.Le rechargement du cache de schéma après une migration
PostgREST garde le schéma Postgres en mémoire au démarrage. Après un DDL (créer une table, ajouter une colonne), ce cache doit être rechargé avant que la nouvelle route ne réponde, et ce rechargement est asynchrone.
Une écriture qui arrive dans cette fenêtre peut recevoir un 404 transitoire (cache pas encore à jour), alors que la table existe bel et bien côté Postgres. Le gateway Aurabase absorbe ça avec une boucle de réessai bornée, vérifiée dans postgrest_proxy.rs : jusqu'à 8 tentatives, backoff croissant (250 ms plus 100 ms par tentative), 3,5 secondes cumulées au pire cas. Ce mécanisme ne joue que sur les écritures, jamais sur les lectures.
pg_notify('pgrst', 'reload schema') émis par le service base de données sur le chemin DDL. Si un chemin de migration oublie d'émettre ce signal, les 8 tentatives s'épuisent sur un cache qui ne changera jamais, un risque documenté tel quel dans le commentaire du code, pas maquillé.Pour un déploiement PostgREST auto-hébergé, la leçon se généralise. Chaque chemin DDL de votre application doit déclencher le rechargement, via NOTIFY ou un signal SIGUSR1 au processus. Sans quoi, une migration produit un pic de latence p99 déguisé en erreurs intermittentes juste après le déploiement.
Ce que l'architecture tranche, pas le débit brut
Les quatre limites documentées ici partagent un point commun : aucune ne se voit sur un test de débit HTTP isolé, et toutes les quatre déterminent pourtant si un déploiement PostgREST tient à l'échelle en production.
- Budget de connexions : plafonne le nombre de tenants actifs simultanément sur un cluster mutualisé, indépendamment du débit par tenant.
- Coût du COUNT exact : grandit avec la table, pas avec la charge ; se contourne avec
planned/estimated. - Troncature silencieuse : un plafond de lignes correctement configuré peut quand même casser une pagination mal instrumentée.
- Rechargement de schéma : une fenêtre de latence après chaque migration, bornée si le signal de reload est bien câblé, illimitée sinon.
Si vous choisissez entre PostgREST auto-hébergé, une couche GraphQL type Hasura, ou une API sur mesure, ces quatre axes sont un meilleur point de comparaison qu'un chiffre de req/s isolé. Voir notre comparatif PostgREST vs Hasura vs API custom. Le choix du pooler qui se tient devant votre base compte autant : notre comparatif PgBouncer vs Supavisor vs PgCat détaille pourquoi PostgREST ne peut pas passer par un pooler en mode transaction.
FAQ
Ce qu'il faut retenir
PostgREST ne casse presque jamais sous la charge HTTP seule : son architecture est trop simple pour ça. Ce qui casse en production, c'est ce qui l'entoure : combien de connexions ses réplicas tiennent ouvertes, combien coûte un total exact. Ça inclut aussi si une troncature reste visible, et combien de temps dure la fenêtre après une migration.
Ces quatre limites ne sont pas propres à Aurabase : elles s'appliquent à tout déploiement PostgREST, auto-hébergé ou managé. Ce que ce code montre, c'est comment un déploiement multi-tenant les rend explicites plutôt que de les laisser surprendre en production.