Les architectures serverless comme AWS Lambda s'adaptent instantanément pour gérer les pics de trafic. Cependant, las bases de données relationnelles traditionnelles comme PostgreSQL et MySQL n'ont pas été conçues pour ce modèle d'exécution éphémère. Elles s'attendent à un pool stable de connexiones à longue durée de vie, et non à des milliers de connexions créées et détruites en quelques secondes.
Lorsque des centaines ou des milliers de fonctions Lambda démarrent simultanément, elles peuvent facilement saturer votre base de données, entraînant l'épuisement du pool de connexions, des pannes en cascade et des interruptions de service.
La cause originelle : Les tempêtes de connexion
Dans une architecture serveur traditionnelle, une seule instance d'application établit un pool de connexions (par exemple, 20 connexions) qu'elle partage entre toutes les requêtes simultanées. Trois instances derrière un répartiteur de charge signifient 60 connexions au total : un chiffre prévisible et facile à gérer.
Dans un modèle serverless, ce contrat est rompu :
- Chaque instance active de Lambda s'exécute dans son propre conteneur isolé.
- Chaque conteneur établit sa propre connexion à la base de données.
- Si votre application monte en charge à 1 000 exécutions simultanées, vous effectuerez 1 000 requêtes de connexion distinctes à la base de datos.
- La plupart des instances PostgreSQL acceptent par défaut un maximum de 100 connexions (
max_connections = 100). Un RDSdb.t3.mediumsupporte environ 150 connexions. Vos 1 000 instances Lambda dépasseront cette limite instantanément, renvoyant l'erreurFATAL: too many connections.
Le problème est amplifié par les démarrages à froid (cold starts) : lors d'un pic de trafic, Lambda lance simultanément de nombreux conteneurs. Chacun tente d'ouvrir une connexion avant même de traiter la moindre requête, provoquant une surcharge de connexions.
Stratégie 1 : Réutilisation des connexions lors des démarrages à chaud (Warm Starts)
L'optimisation la plus simple consiste à déclarer le client de base de données en dehors de la fonction handler. Les conteneurs Lambda persistant entre les appels lors des démarrages à chaud, une connexion établie lors de la première exécution sera réutilisée pour les requêtes suivantes.
const { Client } = require('pg');
// Initialisé en dehors du handler ; réutilisé lors des démarrages à chaud
let client = null;
async function getClient() {
if (!client) {
client = new Client({
connectionString: process.env.DATABASE_URL,
connectionTimeoutMillis: 5000,
statement_timeout: 30000,
});
await client.connect();
}
return client;
}
exports.handler = async (event) => {
const db = await getClient();
try {
const res = await db.query('SELECT id, name FROM users WHERE active = $1', [true]);
return { statusCode: 200, body: JSON.stringify(res.rows) };
} catch (err) {
// Gérer les connexions perdues suite au gel/dégel du conteneur Lambda
if (err.code === 'EPIPE' || err.code === 'ECONNRESET') {
client = null; // Forcer la reconnexion au prochain appel
throw err;
}
throw err;
}
};
Points clés :
- Les requêtes paramétrées (
$1) évitent les injections SQL. N'insérez jamais de variables utilisateur directement dans vos chaînes de requête. connectionTimeoutMillisévite que la fonction Lambda ne reste bloquée indéfiniment si la base de données est saturée.- La gestion des sockets obsolètes (stale connections) est cruciale. Lambda fige les conteneurs inactifs. Si le gel dure trop longtemps, le serveur de base de données peut fermer la connexion sans que le client Node.js n'en soit averti, provoquant des erreurs réseau.
Fermeture propre des connexions
Les conteneurs Lambda finissent par être recyclés. Vous pouvez enregistrer un écouteur pour fermer proprement les connexions actives :
process.on('beforeExit', async () => {
if (client) {
await client.end();
client = null;
}
});
Cela évite d'abandonner des connexions "fantômes" qui encombrent inutilement la base de données.
Stratégie 2 : Utiliser un proxy de connexion
La réutilisation des connexions aide, mais ne règle pas le problème fondamental : N conteneurs Lambda = N connexions. Pour décorréler la scalabilité de Lambda de celle de votre base de données, vous devez interposer un proxy de connexion (connection pooler).
AWS RDS Proxy (Géré)
AWS RDS Proxy se place entre vos fonctions Lambda et votre base de données RDS. Il maintient un pool de connexions persistant vers la base de données et y multiplexe les connexions éphémères de vos fonctions Lambda.
Fonctionnement :
- Votre fonction Lambda se connecte au proxy au lieu de la base de données.
- Le proxy maintient un pool stable (ex: 50 connexions) avec la base de données réelle.
- Lorsqu'un conteneur Lambda effectue une requête, le proxy lui attribue une connexion libre du pool, puis la récupère immédiatement après l'exécution.
- Une autre fonction réutilise la même connexion de base de données l'instant d'après.
Avantages :
- Transparent pour le code de votre application (seule la chaîne de connexion change).
- Supporte l'authentification IAM d'AWS, évitant de stocker des mots de passe dans les variables d'environnement.
- Gère automatiquement le basculement (failover) vers les réplicas en cas de panne de l'instance principale.
Inconvénient : RDS Proxy coûte environ $15-25/mois par proxy + $0,015 par vCPU-heure. Pour les applications de production, ce coût est dérisoire face à la stabilité obtenue.
PgBouncer (Auto-hébergé)
PgBouncer est le pooler open-source le plus populaire pour PostgreSQL, idéal si vous souhaitez garder le contrôle ou si vous déployez hors d'AWS.
PgBouncer fonctionne selon trois modes :
| Mode | Comportement | Usage idéal |
|---|---|---|
| Session | Une connexion serveur dédiée par client pour toute la durée de sa session | Requêtes interactives |
| Transaction | Connexion libérée dès la fin de chaque transaction SQL | La majorité des usages serverless |
| Statement | Connexion libérée dès la fin de chaque requête SQL | Requêtes unitaires uniquement |
En serverless, le mode transaction est presque toujours le bon choix. Il permet à un pool de 50 connexions serveur de traiter les requêtes de milliers de conteneurs Lambda, car les connexions ne sont occupées que pendant l'exécution effective de chaque transaction.
Déploiement :
- Sidecar sur EC2/ECS : Déployez PgBouncer sur une petite instance dans le même VPC que votre base de datos.
- Services gérés : Des hébergeurs comme Supabase, Neon ou Crunchy Bridge intègrent PgBouncer par défaut dans leurs offres PostgreSQL.
Comparatif des solutions de pooling
| Solution | Type | PostgreSQL | MySQL | Tarification | Usage idéal |
|---|---|---|---|---|---|
| AWS RDS Proxy | Géré | ✅ | ✅ | ~$20/mois + vCPU-heures | Projets AWS-native sans maintenance |
| PgBouncer | Auto-hébergé | ✅ | ❌ | Coût de l'instance d'hébergement | Projets multi-cloud avec contrôle total |
| Prisma Accelerate | Géré | ✅ | ✅ | Gratuit puis à l'usage | Projets Node.js utilisant l'ORM Prisma |
| Neon Serverless Driver | Géré | ✅ | ❌ | Inclus avec Neon | Projets utilisant Neon PostgreSQL |
Stratégie 3 : Réduire la taille du pool interne
Si vous utilisez un ORM ou une bibliothèque gérant un pool de connexions (comme pg-pool) dans votre code Lambda, définissez la taille maximale du pool (max) à 1. Cela peut sembler contre-intuitif, mais c'est essentiel :
// Configuration Knex.js pour Lambda
const knex = require('knex')({
client: 'pg',
connection: process.env.DATABASE_URL,
pool: {
min: 0, // Permet de fermer toutes les connexions inactives
max: 1, // Une seule connexion par conteneur Lambda
idleTimeoutMillis: 30000,
reapIntervalMillis: 1000,
},
});
Pourquoi max: 1 ? Un conteneur Lambda ne traite qu'une seule requête à la fois. Il n'y a pas de parallélisme au sein d'un même conteneur. Conserver un pool local de 10 connexions signifie que 9 resteront inutilisées mais bloqueront quand même des slots de connexion sur votre serveur de base de données.
Configurez également
min: 0. Si le conteneur reste actif mais ne reçoit pas de trafic,min: 1maintiendrait une connexion ouverte inutilement. Avecmin: 0, le pool local ferme la connexion après expiration du délai d'inactivité (idleTimeoutMillis).
Stratégie 4 : Gérer les démarrages à froid (Cold Starts)
C'est lors des démarrages à froid que la charge est la plus critique. Lors d'un pic soudain de trafic, Lambda instancie simultanément des dizaines ou centaines de conteneurs, provoquant un afflux massif de demandes de connexion au même instant.
Concurrence provisionnée (Provisioned Concurrency)
Cette option d'AWS Lambda maintient un nombre prédéfini de conteneurs actifs et chauds. Leurs connexions à la base de données étant déjà établies, les requêtes entrantes sont traitées sans délai ni nouvelle connexion.
aws lambda put-provisioned-concurrency-config \
--function-name ma-fonction-api \
--qualifier prod \
--provisioned-concurrent-executions 50
Coût : Vous payez pour les instances actives en permanence (~$0.0000041667/Go-seconde). Pour une fonction de 512 Mo et 50 instances, cela représente environ $2,70/jour, un investissement rentable pour une API de production.
Concurrence réservée (Reserved Concurrency)
Si vous ne pouvez pas utiliser de proxy et devez limiter strictement les connexions, configurez une limite de concurrence réservée sur votre fonction Lambda correspondant à la capacité de la base de données :
aws lambda put-function-concurrency \
--function-name ma-fonction-api \
--reserved-concurrent-executions 100
Si le trafic dépasse cette limite, les requêtes seront rejetées (HTTP 429) au lieu de faire planter le serveur de base de données.
Stratégie 5 : Surveiller l'état des connexions
Pour éviter les pannes en production, vous devez disposer de métriques claires sur les connexions de votre base de données.
Requêtes de surveillance PostgreSQL
Exécutez régulièrement ces requêtes pour auditer l'utilisation des connexions :
-- Nombre de connexions par état
SELECT state, count(*)
FROM pg_stat_activity
WHERE datname = 'votre_base'
GROUP BY state;
-- Connexions par application (pour identifier la fonction Lambda responsable)
SELECT application_name, count(*), state
FROM pg_stat_activity
WHERE datname = 'votre_base'
GROUP BY application_name, state
ORDER BY count DESC;
-- Détecter les connexions inactives depuis plus de 5 minutes (fuites potentielles)
SELECT pid, usename, application_name, state,
now() - state_change AS idle_duration
FROM pg_stat_activity
WHERE state = 'idle'
AND now() - state_change > interval '5 minutes';
Alertes CloudWatch
Définissez des alertes sur les indicateurs suivants :
DatabaseConnections(RDS) : Alerte à 80 % demax_connectionspour intervenir avant la panne.- Lambda
Throttles: Si vous utilisez la concurrence réservée, pour savoir quand votre trafic s'approche des limites configurées. - Lambda
ConcurrentExecutions: Pour suivre le nombre de conteneurs actifs et dimensionner vos pools.
Astuce : Spécifiez le paramètre de connexion
application_namedans votre client SQL pour y inclure le nom de votre fonction Lambda. Cela rendra les rapports depg_stat_activityimmédiatement lisibles.
const client = new Client({
connectionString: process.env.DATABASE_URL,
application_name: `${process.env.AWS_LAMBDA_FUNCTION_NAME}-${process.env.AWS_LAMBDA_FUNCTION_VERSION}`,
});
Conclusion
Faire monter en charge des architectures serverless impose d'adapter sa gestion des bases de données relationnelles. Le modèle traditionnel (pools locaux partagés et persistants) n'est plus adapté à des conteneurs isolés et éphémères.
Une architecture robuste repose sur la combinaison de ces méthodes :
- Réutiliser les connexions sur les conteneurs actifs (warm starts).
- Utiliser un proxy de connexion (RDS Proxy ou PgBouncer) pour isoler la base de données.
- Limiter la taille des pools locaux à un maximum de 1.
- Dimensionner la concurrence Lambda (provisionnée ou réservée) pour amortir les pics de charge.
- Surveiller activement les connexions via
pg_stat_activityet des alertes.
Dans la pratique, l'utilisation d'un proxy reste la solution la plus efficace, complétée par les optimisations de code pour garantir une stabilité maximale.
