Retour au blog
Performances & Benchmarks · Vérifié dans le code · 11 min de lecture

PostgREST : benchmark et limites réelles en production

Affane Daylami · Fondateur· 24 août 2026

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.

L'essentiel
  • 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ême max_connections.
  • Prefer: count=exact force un scan MVCC coûteux sur les grandes tables. PostgREST documente deux alternatives moins chères : count=planned et count=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 dans Content-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.
#
Méthodologie

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.

#
Vérifié dans le code

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.

50-250m
CPU PAR RÉPLICA
requests → limits
64-128
MO DE RAM PAR RÉPLICA
requests → limits
2
RÉPLICAS PAR PROJET
haute dispo (P22)

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 :

PalierPGRST_DB_POOL / réplicaRéplicasConnexions / projet éveillé
Dédié (premium, A1)10 (défaut PostgREST)220
Mutualisé (flotte, free/pro/team)2 (défaut Aurabase, abaissé)24
deploy/cnpg/tenant-postgrest.yaml (extrait réel, valeur substituée par le provisionneur)
YAML
# Empreinte de connexions par réplica sur le primaire.
env:
- { name: PGRST_DB_POOL, value: "PGRST_DB_POOL_VALUE" } # 10 (dédié) ou 2 (mutualisé)

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 vrai plafond

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.

Budget de projets simultanément actifs par palier, cluster Postgres mutualiséPalier free : 5 projets actifs simultanés (max_connections 50, pooler 20). Palier pro : 7 (max_connections 100, pooler 60). Palier team : 10 (max_connections 200, pooler 150). Formule dérivée du code Aurabase (fleet.rs::derive_wake_budget), réserve fixe de 10 connexions, 4 connexions par projet éveillé.024681012free (max_connections 50)5 projetspro (max_connections 100)7 projetsteam (max_connections 200)10 projets

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.

#
Coût caché

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.

terminal
BASH
# Coûteux sur une grande table : force un scan MVCC du résultat filtré
curl "https://<gateway>/v1/db/<project_id>/orders?status=eq.paid" \
-H "apikey: <clé>" -H "Prefer: count=exact"
# Alternatives moins coûteuses, documentées par PostgREST
-H "Prefer: count=planned" # estimation via le planificateur
-H "Prefer: count=estimated" # planned au-delà d’un seuil, exact en dessous

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.

#
Mesuré en réel

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 :

RequêteLignes renduesContent-Rangemeta (Aurabase)
?limit=50 (sans count)5 / 10 réelles0-4/*{}
?limit=50&count=exact5 / 10 réelles0-4/10{ total: 10 }

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.

Conséquence pour toute pagination sur PostgREST
Si votre déploiement PostgREST fixe un 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.
#
Latence différée

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.

Un détail que le code documente lui-même
Le gateway n'émet aucun signal de rechargement : il attend seulement. Le seul déclencheur réel est un 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.

#
Synthèse

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.

#
Questions Fréquentes

FAQ

PostgREST est-il assez rapide pour de la production à grande échelle ?+
PostgREST lui-même est un processus léger. Sur les instances dédiées Aurabase, une réplique tourne avec 50 à 250 millicores de CPU et 64 à 128 Mo de RAM, vérifié dans le manifeste Kubernetes du projet. Le débit HTTP brut n’est presque jamais le facteur limitant en production. Ce sont le budget de connexions Postgres, le coût d’un COUNT exact et le cache de schéma qui déterminent si l’ensemble tient à l’échelle, pas la vitesse du binaire PostgREST seul.
Comment savoir si ma réponse PostgREST a été tronquée par db-max-rows ?+
Le header Content-Range renvoyé par PostgREST ne le dit jamais. Une réponse plafonnée à 5 lignes par db-max-rows est indiscernable d’une table qui n’en contient réellement que 5, mesuré en conditions réelles sur une instance Aurabase dédiée. La seule façon fiable de le détecter est de comparer le nombre de lignes reçues au total renvoyé par Prefer: count=exact, sans ce header la troncature reste invisible.
Le COUNT exact ralentit-il toujours une requête PostgREST ?+
Prefer: count=exact force Postgres à compter les lignes visibles du résultat filtré à chaque requête, un coût qui croît avec la taille de la table à cause du MVCC. Postgres ne maintient pas de compteur de lignes indexé prêt à l’emploi. PostgREST propose deux alternatives moins coûteuses, count=planned (estimation via le planificateur) et count=estimated (bascule automatique au-delà d’un seuil), documentées dans sa documentation officielle.
Combien de connexions Postgres PostgREST consomme-t-il ?+
Ça dépend entièrement de PGRST_DB_POOL multiplié par le nombre de réplicas. Vérifié dans le code Aurabase : une instance dédiée (palier premium) ouvre par défaut 10 connexions par réplica, soit 20 au total sur 2 réplicas. Le palier mutualisé abaisse volontairement ce pool à 2 par réplica, soit 4 connexions par projet éveillé, pour faire tenir plus de tenants sur le même budget max_connections du cluster partagé.
Existe-t-il un benchmark officiel de PostgREST ?+
Le projet maintient un dépôt dédié, 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. Cet article documente des limites architecturales vérifiées dans notre code et dans la documentation officielle de PostgREST, pas un banc que nous aurions reproduit nous-mêmes.
#
Conclusion

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.

ARCHITECTURE VÉRIFIABLE

Un PostgREST réel, avec son budget de connexions déjà dimensionné.

Créez un projet Aurabase et inspectez vous-même les en-têtes Content-Range, le comportement de count=exact et le rechargement de schéma après une migration.

Créer un projet Documentation base de données
Aucune carte bancaire requise · 500 MB gratuits · 50 000 MAU