Qu’est-ce qu’un pool de connexions ?
Un pool de connexions est un cache de connexions à la base de données ouvertes que l’application emprunte et restitue. Au lieu d’ouvrir une nouvelle connexion pour chaque requête, une requête en récupère une, exécute ses instructions, puis la rend au pool. La requête suivante réutilise le même socket, déjà authentifié et prêt à l’emploi.
C’est important car une connexion à une base de données n’est pas gratuite. Sur une base de données de test locale, cela peut sembler instantané, mais en production, chaque nouvelle connexion engendre un coût de configuration fixe avant d’effectuer le moindre travail utile. Un pool paie ce coût une seule fois par connexion plutôt qu’une fois par requête, c’est pourquoi c’est l’un des premiers éléments d’infrastructure que presque tous les backends mettent en place.
Le pool sert également de mécanisme de sécurité. Comme il possède un maximum strict, il ne peut pas ouvrir accidentellement dix mille connexions lors d’un pic de trafic. Les appels qui arrivent lorsque le pool est plein attendent dans une file d’attente, ce qui ralentit le processus mais reste gérable, contrairement à une saturation de la base de données, qui serait fatale.
Un pool en quelques lignes
Dans Node.js, le driver pg inclut un pool prêt à l’emploi. Pour en créer un, un simple appel au constructeur suffit, et chaque requête exécutée via celui-ci emprunte et restitue automatiquement une connexion.
import { Pool } from "pg";
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 5_000,
});
const { rows } = await pool.query("SELECT id, email FROM users WHERE id = $1", [id]);
C’est tout le concept. Le pool ouvre les connexions de manière paresseuse (lazy) au fur et à mesure des besoins, les maintient ouvertes entre les requêtes et les ferme lorsqu’elles sont restées inactives trop longtemps. L’application ne voit jamais de socket ; elle voit une méthode query.
Ce même modèle existe dans tous les écosystèmes. Java a HikariCP, Python a le pool de SQLAlchemy et celui d’asyncpg, Go a database/sql avec SetMaxOpenConns, et chaque ORM en encapsule un. Les noms diffèrent, mais les paramètres restent les mêmes : un maximum, un minimum, un délai d’expiration d’inactivité (idle timeout) et un délai d’attente d’acquisition (checkout timeout).
Pourquoi les connexions sont coûteuses
Le coût correspond à une succession d’étapes, dont chacune nécessite un travail réel de la part du réseau ou du serveur de base de données.
- Handshake TCP — un aller-retour pour établir le socket. Sur une base de données distante avec un RTT de 30 ms, cela représente déjà 30 ms.
- Négociation TLS — si la connexion est chiffrée, plusieurs allers-retours supplémentaires sont nécessaires pour s’accorder sur les clés. Un délai additionnel de 50 à 100 ms est courant.
- Authentification — le client prouve son identité, souvent via un hash de mot de passe que le serveur doit calculer. L’authentification SCRAM effectue délibérément un travail coûteux.
- Processus backend — c’est un coût spécifique à Postgres. Postgres crée un nouveau processus système (fork) pour chaque connexion, chacun ayant sa propre mémoire. MySQL utilise un thread, ce qui est plus léger mais n’est pas gratuit pour autant.
- Configuration de la session — les chemins de recherche (search paths), le fuseau horaire, le nom de l’application et d’autres paramètres doivent être appliqués.
En additionnant tout cela, une nouvelle connexion peut coûter plusieurs dizaines de millisecondes avant même la première requête. Si une page effectue dix requêtes, ouvrir une connexion par requête deviendrait l’étape la plus lourde de la requête. Pire encore, le modèle “un processus par connexion” signifie que les connexions inactives consomment toujours de la mémoire ; ainsi, quelques centaines de connexions peuvent réellement pénaliser le serveur.
Un pool transforme tout cela en un coût unique. La connexion est créée une seule fois, utilisée pour des milliers de requêtes, puis fermée lorsque le pool décide qu’elle est trop ancienne ou inactive depuis trop longtemps.
Le cycle de vie d’une connexion poolée
Chaque emprunt suit les quatre mêmes étapes, et les comprendre permet d’expliquer presque tous les comportements de pool que vous devrez déboguer.
Acquisition. L’appelant demande une connexion au pool. Si l’une d’elles est inactive, elle est remise immédiatement. Si toutes sont occupées mais que le pool est inférieur à max, une nouvelle connexion est ouverte. Si le pool a atteint max, l’appelant attend dans une file FIFO jusqu’à ce qu’une connexion soit libérée. Cette attente est le signal que le pool est saturé.
Utilisation. L’appelant exécute une ou plusieurs requêtes sur la connexion. C’est là que la connexion est réellement utile. C’est aussi là que les erreurs surviennent : une transaction laissée ouverte, un client jamais libéré, ou une requête sans timeout.
Libération. L’appelant retourne la connexion au pool. Cela doit impérativement se produire dans un bloc finally, car une exception survenant entre l’acquisition et la libération provoque une fuite de connexion permanente. Une connexion fuitée reste invisible jusqu’à ce que le pool soit vidé et que chaque requête commence à expirer (timeout).
Nettoyage (Reap). Les connexions qui restent inactives plus longtemps que idleTimeoutMillis, ou qui dépassent une durée de vie maximale, sont fermées. Cela évite que le pool n’accapare des ressources que la base de données pourrait utiliser ailleurs, et c’est ainsi que les connexions obsolètes datant d’avant un redémarrage sont retirées.
const client = await pool.connect();
try {
return await client.query("SELECT now()");
} finally {
client.release();
}
De nombreux drivers vous permettent de sauter l’étape d’emprunt explicite pour les requêtes simples — pool.query() gère l’acquisition et la libération pour vous — ce qui élimine la source la plus courante de fuites. N’utilisez la forme explicite que lorsque vous avez besoin de plusieurs instructions sur la même connexion.
Récupération, requête et libération en toute sécurité
Le modèle en trois lignes présenté ci-dessus constitue la base de toute interaction sécurisée avec un pool, mais le code de production nécessite un peu plus de rigueur.
Enveloppez l’ensemble du processus de récupération dans un helper pour qu’aucun appelant ne puisse oublier la libération. Le helper gère le try/finally et la gestion des erreurs, tandis que le reste de la base de code se contente d’attendre l’exécution d’une fonction.
export async function withClient<T>(
fn: (client: PoolClient) => Promise<T>,
): Promise<T> {
const client = await pool.connect();
try {
return await fn(client);
} finally {
client.release();
}
}
await withClient((client) =>
client.query("UPDATE jobs SET status = 'done' WHERE id = $1", [id]),
);
Réfléchissez à ce qui se passe lorsque la connexion elle-même est rompue. Une requête peut échouer parce que le SQL est incorrect, ce qui est le problème de l’appelant, ou parce que le socket est tombé, ce qui est le problème du pool. Le second cas mérite une tentative de reconnexion (retry), mais seulement si l’opération peut être répétée sans risque. Un SELECT le peut ; un INSERT sans clé d’idempotence ne le peut pas.
async function queryWithRetry(text: string, params: unknown[], retries = 2) {
for (let attempt = 0; ; attempt++) {
try {
return await pool.query(text, params);
} catch (err) {
const isConnectionError = (err as { code?: string }).code === "ECONNRESET";
if (!isConnectionError || attempt >= retries) throw err;
}
}
}
Enfin, définissez un timeout de requête (statement timeout) sur la connexion afin qu’une seule requête incontrôlée ne puisse pas monopoliser un slot du pool indéfiniment. Sur Postgres, statement_timeout annule la requête côté serveur ; sans cela, le client attend aussi longtemps que la base de données.
Dimensionner le pool
La tentation est de régler max sur une valeur élevée pour que rien n’ait jamais à attendre. C’est exactement l’inverse de ce que vous voulez. Une base de données dispose d’un nombre limité de cœurs CPU et d’une quantité de mémoire limitée ; elle ne peut pas exécuter plus de requêtes en parallèle que ce que sa capacité permet. Des connexions supplémentaires n’augmentent pas le débit ; elles ajoutent du changement de contexte (context switching), de la contention de verrouillage et de la pression sur la mémoire.
La règle empirique classique provient du wiki de PostgreSQL :
connections = (cores × 2) + effective_spindle_count
Pour une base de données à quatre cœurs avec un stockage SSD, cela représente environ huit à dix connexions. Cela semble alarmant, mais c’est correct : une requête bien indexée s’exécute en une ou deux millisecondes, donc une poignée de connexions peut servir des milliers de requêtes par seconde. Cette formule est un point de départ, pas une loi : mesurez et ajustez.
La seconde partie du calcul est celle que les gens oublient. Chaque instance d’application exécute son propre pool, le budget de la base de données doit donc être divisé par le nombre d’instances :
pool max per instance = database budget / number of instances
Dix instances avec un pool de vingt demandent deux cents connexions à la base de données, ce qui dépasse presque certainement max_connections. Soit vous réduisez le pool par instance, soit vous placez un pooler en amont.
Enfin, vérifiez les propres limites de la base de données. Postgres règle max_connections par défaut à 100, et les connexions réservées aux superutilisateurs et à la réplication réduisent ce qui est réellement disponible. Si vous demandez plus que cela, vous obtiendrez des erreurs, et non une dégradation progressive du service.
Le problème du fan-out avec un pool par instance
C’est la panne de connexion la plus courante dans les déploiements modernes, et elle survient progressivement.
Un service démarre sur une seule instance avec un pool de vingt connexions. Tout fonctionne. Le trafic augmente, le service passe donc à cinq instances — et maintenant, la base de données voit cent connexions, exactement à la limite. Passez à dix instances et on arrive à deux cents, dépassant largement la capacité. Rien n’a changé dans l’application ; c’est le fan-out qui a évolué.
Ce schéma se répète dans Kubernetes, dans le serverless et partout où les processus se multiplient. Un pool limite les connexions par processus, et non par système. La solution consiste soit à utiliser un petit pool par instance dimensionné en fonction de la flotte, soit à utiliser un pooler côté serveur qui présente un seul petit pool à la base de données, quel que soit le nombre de clients connectés.
10 instances × pool max 20 = 200 database connections
↓
database max_connections = 100
↓
"too many clients already"
Un exemple concret de dimensionnement
Les chiffres permettent de rendre les compromis concrets. Supposons que la base de données soit une instance managée de quatre cœurs avec stockage SSD et le max_connections par défaut de 100, et que le service s’exécute sur huit instances d’application.
La règle empirique donne un budget d’environ dix connexions que la base de données peut réellement utiliser en parallèle. Réparties sur huit instances, cela représente un pool d’une ou deux connexions par instance — bien moins que les dix généralement configurées, et souvent suffisant pour une charge de travail rapide et bien indexée. Si cela semble trop restrictif, la solution n’est pas d’augmenter le pool, mais de placer un pooler en amont afin que les huit petits pools partagent un ensemble contrôlé de backends.
database budget ≈ 10 connections
app instances = 8
pool max per instance = 10 / 8 ≈ 1
too small to be useful → add PgBouncer
PgBouncer default_pool_size = 10
app pool max (per instance) = 5 # clients may wait; backends stay bounded
L’idée clé est que le pool de l’application et le budget de la base de données sont deux chiffres différents. Le pool de l’application contrôle le nombre de requêtes que chaque instance peut exécuter simultanément ; le pooler contrôle le nombre de connexions serveur qui existent réellement. Configurer le pool de l’application légèrement au-dessus de la part allouée par instance permet à une instance de gérer des pics de charge tandis que le pooler protège la base de données.
Laissez toujours une marge de manœuvre. Les bases de données managées réservent certaines connexions pour l’administration et la réplication, et un basculement (failover) nécessite brièvement davantage de ressources. Viser 70 à 80 % de max_connections laisse de la place pour les migrations, le monitoring et un déploiement problématique.
Maintenir la santé des connexions
Les connexions longue durée peuvent devenir obsolètes. Un redémarrage de la base de données, un basculement (failover), une partition réseau ou un délai d’expiration d’inactivité du pare-feu peuvent couper le socket sans que l’application ne s’en aperçoive. La requête suivante sur cette connexion échouera et, sans gestion appropriée, cet échec sera déroutant.
Trois paramètres permettent de gérer cela :
idleTimeoutMillisferme les connexions qui n’ont pas été utilisées pendant un certain temps. Des valeurs courtes permettent de garder le serveur propre, mais risquent de forcer la réouverture de connexions lors des périodes calmes ; trente secondes constituent généralement un bon compromis.- Une durée de vie maximale (maximum lifetime) retire les connexions après un âge fixe, quel que soit leur usage. Cela permet d’échelonner les reconnexions au lieu de laisser toutes les connexions expirer simultanément lors d’un basculement.
- La validation effectue une vérification légère, telle que
SELECT 1, avant de distribuer une connexion, afin qu’un socket mort soit écarté plutôt que transmis à l’appelant.
Même avec ces trois mécanismes, gérez explicitement les erreurs de connexion. Un client inactif qui génère une erreur doit être retiré du pool, et le driver émet généralement un événement error précisément pour cela :
pool.on("error", (err) => {
console.error("unexpected idle client error", err);
});
Ignorer cet événement transforme un incident mineur récupérable en une exception non gérée capable de faire planter le processus.
Les transactions nécessitent une seule connexion
Une transaction est liée à une seule connexion. Chaque BEGIN, instruction et COMMIT doit être exécuté sur le même client, car l’état de la transaction réside dans ce processus backend. C’est précisément là que le pooling et les transactions interagissent, et que le code naïf échoue.
Le mode de défaillance est subtil : si vous exécutez BEGIN via pool.query(), le pool peut vous attribuer une connexion différente pour l’instruction suivante, et le COMMIT échouera ou ne validera rien. La règle est simple : récupérez un client pour l’ensemble de la transaction et ne le libérez qu’après le commit ou le rollback.
const client = await pool.connect();
try {
await client.query("BEGIN");
await client.query("UPDATE accounts SET balance_cents = balance_cents - $1 WHERE id = $2", [100, from]);
await client.query("UPDATE accounts SET balance_cents = balance_cents + $1 WHERE id = $2", [100, to]);
await client.query("COMMIT");
} catch (err) {
await client.query("ROLLBACK");
throw err;
} finally {
client.release();
}
Comme une transaction monopolise une connexion pendant toute sa durée, les transactions longues réduisent la taille effective du pool pour tous les autres utilisateurs. Gardez-les courtes, évitez les appels réseau à l’intérieur et définissez un statement_timeout pour éviter qu’une requête incontrôlée ne bloque une connexion indéfiniment.
Requêtes préparées et pool de connexions
Les requêtes préparées (prepared statements) sont une fonctionnalité de performance : la base de données analyse et planifie une requête une seule fois, puis réutilise ce plan. C’est également là que la gestion du pool devient subtile, car une requête préparée est liée à une connexion backend spécifique.
Avec un pool interne au processus, cela ne pose généralement pas de problème. Des drivers comme pg préparent une requête sur la connexion actuellement utilisée, et le plan n’est réutilisé que lorsque cette même connexion traite à nouveau la requête. Rien ne plante, mais le gain de performance est irrégulier et le driver doit gérer un cache croissant de requêtes nommées.
Le problème surgit lorsque vous combinez des requêtes préparées nommées avec un pooler en mode transaction (transaction-mode). Le pooler peut envoyer votre requête vers un backend différent de celui qui a préparé la requête ; le serveur ne reconnaît alors pas le nom et renvoie une erreur. C’est la surprise la plus courante lors de l’utilisation de PgBouncer.
Les solutions sont simples :
- Désactivez les requêtes préparées côté serveur dans le driver lors de l’utilisation du pooling de transactions, pour qu’il envoie des requêtes non nommées. De nombreux drivers possèdent un flag dédié à cet effet.
- Ou utilisez le pooling de session (session pooling), qui maintient un client sur un seul backend et rend à nouveau les requêtes préparées sûres.
- Ou activez
max_prepared_statementssur les versions récentes de PgBouncer, qui proxy correctement le protocole de préparation.
Le principe général est que tout élément stocké dans la session serveur est fragile avec le pooling de transactions. Les requêtes préparées, les variables SET, les tables temporaires et les verrous consultatifs (advisory locks) appartiennent tous à une connexion, et un pooler est libre de vous en attribuer une différente la fois suivante.
Serverless et épuisement des connexions
Les fonctions Serverless représentent le pire scénario pour le connection pooling. Chaque invocation peut s’exécuter dans un conteneur éphémère et isolé, sans mémoire partagée, ce qui empêche la réutilisation d’un pool créé par une autre invocation. Sous forte charge, des centaines de fonctions concurrentes ouvrent chacune une connexion, provoquant une “tempête de connexions” que la base de données ne peut pas supporter.
La solution consiste à utiliser un pooler côté serveur entre les fonctions et la base de données. PgBouncer, ou un équivalent managé tel que RDS Proxy ou le pooler intégré d’un fournisseur, maintient un petit ensemble de connexions réelles et multiplexe les nombreuses connexions clients éphémères sur celles-ci.
1000 concurrent functions ──► PgBouncer ──► 20 Postgres connections
(clients) (pooler) (backends)
Il reste utile d’avoir un pool à l’intérieur d’une fonction, mais il doit être minuscule — souvent une seule connexion — et configuré pour se fermer rapidement. En effet, un conteneur qui maintient des connexions ouvertes alors qu’il est inactif gaspille les ressources de la base de données. C’est le pooler qui permet de rendre l’infrastructure viable.
PgBouncer et le pooling de transactions
PgBouncer est le pooler externe standard pour Postgres. Il utilise le protocole réseau de Postgres, donc les applications s’y connectent exactement comme elles le feraient avec la base de données. Il propose trois modes de pooling, et le choix a des conséquences réelles.
Le Session pooling assigne une connexion serveur pour toute la durée de la session du client. C’est le mode le plus compatible — SET, LISTEN, les advisory locks et les prepared statements se comportent normalement — mais c’est celui qui multiplexe le moins, car un client conserve sa connexion même lorsqu’il est inactif.
Le Transaction pooling assigne une connexion serveur uniquement pour la durée d’une transaction et la restitue au pool lors du commit. Mille clients peuvent ainsi partager vingt backends, c’est pourquoi c’est le choix par défaut pour les applications web. Le compromis est que l’état de la session ne survit pas entre les transactions : un SET peut atterrir sur un backend différent la fois suivante, les advisory locks maintenus entre plusieurs instructions ne fonctionnent plus, et les prepared statements côté serveur peuvent entrer en collision.
Le Statement pooling restitue la connexion après chaque instruction. C’est le mode qui multiplexe le plus et le plus restrictif ; les transactions multi-instructions ne sont pas autorisées. C’est rarement ce dont vous avez besoin.
[databases]
shop = host=127.0.0.1 port=5432 dbname=shop
[pgbouncer]
pool_mode = transaction
default_pool_size = 20
max_client_conn = 1000
server_idle_timeout = 60
Si vous utilisez le mode transaction, auditez votre ORM et vos requêtes. Utilisez SET LOCAL à l’intérieur d’une transaction au lieu de SET, évitez de maintenir des advisory locks entre plusieurs instructions, et configurez le driver pour désactiver les prepared statements côté serveur ou utilisez des instructions non nommées.
Surveiller la santé du pool
Un pool possède quatre indicateurs à surveiller qui, ensemble, vous indiquent s’il est correctement dimensionné.
- Le temps d’attente d’une connexion. Le signal le plus direct de saturation. Si les appels attendent régulièrement, le pool est trop petit pour la charge, ou un élément conserve les connexions trop longtemps.
- Connexions actives versus inactives. Un pool qui atteint systématiquement son maximum avec toutes les connexions actives est sous-dimensionné. Un pool majoritairement inactif est surdimensionné et gaspille la mémoire de la base de données.
- Total versus maximum. À quel point le pool est proche de son plafond. Être constamment proche de
maxsignifie que le prochain pic de trafic provoquera une mise en file d’attente. - Erreurs et timeouts. Échecs de connexion, échecs de validation et expirations de
connectionTimeoutMillis. Une augmentation de ces chiffres pointe vers un problème de réseau, de base de données ou de connexions obsolètes.
Côté base de données, pg_stat_activity affiche chaque connexion et son état, ce qui est le moyen le plus rapide de voir si les connexions inactives s’accumulent. Combinez les deux vues : les métriques de l’application vous indiquent comment le pool se comporte, et les métriques de la base de données vous indiquent l’impact sur le serveur.
SELECT state, count(*)
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY state
ORDER BY count(*) DESC;
Un pool d’application sain présente un petit nombre de connexions active et quelques connexions idle. Une accumulation croissante de lignes idle in transaction est un schéma dangereux : ces connexions sont extraites du pool mais ne font rien, souvent parce qu’une transaction a été ouverte sans jamais être validée (commit). Elles occupent des slots du pool et, sur Postgres, peuvent bloquer le vacuum. Considérez l’augmentation de ce nombre comme un bug, et non comme un problème de réglage.
Les pools d’ORM et le pooler
Les ORM ne suppriment pas le besoin de pooling ; ils le masquent. Prisma, TypeORM, Drizzle et Knex maintiennent chacun leur propre pool et proposent une option connectionLimit ou pool. C’est pratique, mais cela signifie que le même calcul de dimensionnement s’applique et que le même problème de fan-out persiste.
L’erreur consiste à faire fonctionner un pool d’ORM et un pooler sans les coordonner. Le pool de l’ORM détermine le nombre de connexions qu’une instance souhaite utiliser ; le pooler détermine combien la base de données autorise. Si le pool de l’ORM est volumineux et que le default_pool_size du pooler est restreint, l’ORM détiendra des connexions que le pooler ne pourra pas servir, et les requêtes s’accumuleront dans la file d’attente du pooler plutôt que de l’ORM. Configurez les deux délibérément, et privilégiez un pool d’ORM modeste derrière un pooler plutôt qu’un pool volumineux sans pooler.
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 5_000,
});
Un pool n’est pas un cache
Il est important de souligner la différence, car les deux sont souvent confondus. Un cache stocke des résultats pour vous éviter d’exécuter une requête. Un pool stocke des connexions pour que vous puissiez exécuter des requêtes à moindre coût. L’ajout d’un pool ne réduit en rien le nombre de requêtes ; il rend simplement chaque requête moins coûteuse à initialiser.
Si une page exécute vingt requêtes, un pool rend ces vingt requêtes rapides, mais il ne les fait pas disparaître. Le niveau d’optimisation suivant consiste à mettre en cache les résultats coûteux, ce qui est traité dans le guide sur le Caching. Les deux se complètent parfaitement : un pool maintient le faible coût des requêtes restantes, tandis qu’un cache élimine celles que vous pouvez éviter entièrement.
Cette distinction explique également une déception courante. Des équipes ajoutent un pool, constatent une baisse de la latence, puis se demandent pourquoi le débit (throughput) reste inchangé sous une charge lourde. Le pool a supprimé le surcoût de connexion, mais la base de données effectue toujours tout le travail. Seul un cache, un meilleur index ou une réduction du nombre de requêtes peut changer cela.
Tester avec un pool
Les tests doivent emprunter le même chemin de pooling qu’en production, car les bugs les plus critiques — comme un client qui fuit ou une transaction sur la mauvaise connexion — n’apparaissent qu’à travers le pool.
Utilisez un seul pool partagé pour l’ensemble de la suite de tests et fermez-le une seule fois à la fin. Ouvrir un pool par fichier de test est lent et peut atteindre la limite de connexions de la base de données lorsque les tests s’exécutent en parallèle.
import { afterAll } from "vitest";
import { pool } from "../src/db.js";
afterAll(async () => {
await pool.end();
});
test("findUser returns null for a missing id", async () => {
const { rows } = await pool.query("SELECT * FROM users WHERE id = $1", ["nope"]);
expect(rows).toHaveLength(0);
});
Orientez vos tests vers une base de données jetable ou une transaction qui effectue un rollback, et vérifiez le comportement du pool là où c’est essentiel : un test qui vérifie pool.totalCount et pool.idleCount avant et après une opération détectera un client qui fuit, ce qu’une assertion classique ne verrait pas. Sur Postgres, pg_stat_activity peut confirmer que le nombre de connexions est revenu à son niveau initial.
Bonnes pratiques
- Utilisez toujours un pool ; n’ouvrez jamais une connexion par requête.
- Dimensionnez le pool en fonction de la capacité de la base de données, puis divisez par le nombre d’instances.
- Configurez un
connectionTimeoutMillispour que les appels échouent rapidement au lieu de rester en attente. - Définissez un timeout d’inactivité (idle timeout) et une durée de vie maximale (maximum lifetime) afin que les connexions obsolètes soient retirées.
- Libérez les connexions dans un bloc
finally, et gérez l’événementerrordu pool. - Conservez une seule connexion pour l’ensemble d’une transaction et gardez les transactions courtes.
- Placez un pooler devant Postgres dès que le nombre d’instances augmente ou que vous utilisez des fonctions serverless.
- Utilisez le transaction pooling pour les applications web et auditez les fonctionnalités liées à la session.
- Surveillez le temps d’attente, le nombre de connexions actives par rapport aux connexions inactives et les timeouts, et pas seulement la latence des requêtes.
- Coordonnez la taille du pool de l’ORM avec la taille du pool du pooler.
Erreurs courantes
- Créer un
Clientpar requête au lieu d’utiliser un pool. - Régler
maxà plusieurs centaines en appelant cela de l’optimisation. - Oublier de libérer un client, ce qui vide progressivement le pool.
- Exécuter
BEGINetCOMMITviapool.query()sur des connexions différentes. - Maintenir une transaction ouverte pendant un appel HTTP ou une interaction utilisateur.
- Laisser
connectionTimeoutMillisà zéro, forçant ainsi les requêtes à attendre indéfiniment. - Ignorer l’événement
errordu pool et subir un crash à cause d’un client inactif expiré. - Utiliser un pool interne dans des fonctions serverless sans pooler en amont.
- Supposer que le mode transaction de PgBouncer supporte les prepared statements et l’état de session.
- Surveiller la latence des requêtes alors que le temps d’attente du pool augmente sans être détecté.
Pour aller plus loin
Le pooling est indissociable de la base de données qu’il dessert, c’est pourquoi le guide PostgreSQL traite en profondeur de max_connections, PgBouncer et du modèle de processus par connexion. Avant d’optimiser le pool, vérifiez si la requête doit réellement être exécutée — le guide sur le Caching explique comment réduire la charge à la source. Si vous utilisez des workers, la section Batch Processing explique comment éviter qu’une flotte de workers n’épuise la base de données, et il peut être utile de se replonger dans Node.js pour comprendre comment l’event loop et les E/S asynchrones interagissent avec un pool.