El pooling de conexiones serverless es una técnica que comparte pocas conexiones reales de base de datos entre muchas instancias de función efímeras. Existe porque dos modelos de escalado discrepan: el cómputo serverless responde a la carga multiplicando instancias, mientras la base de datos trata cada conexión como un recurso caro, respaldado por memoria y con techo rígido. Junta los dos de forma ingenua y un pico de tráfico modesto se convierte en una caída con too many connections.
Puntos clave
| Pregunta | Respuesta |
|---|---|
| La colisión | Las funciones escalan clonando; las conexiones tienen techo y cuestan caro |
| El síntoma | FATAL: sorry, too many clients already durante los picos |
| El arreglo | Un pooler multiplexa muchos clientes sobre pocas conexiones reales |
| La perilla | Transaction vs. session pooling — compartir vs. compatibilidad |
| La respuesta del BaaS | Los clientes hablan HTTPS sin estado; la plataforma es dueña del pool |
La falla, observada desde la base de datos
-- Lo que la base ve durante un pico de tráfico serverless:
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;
-- active | 14
-- idle | 483 ← estacionadas por instancias calientes
SHOW max_connections; -- 100 ← el techo que ya reventaron
-- Cada conexión es un proceso real reteniendo memoria real —
-- por eso el techo existe, y por eso subirlo no es el arreglo.
La forma del problema: cada instancia de función abre su propia conexión, la retiene durante el ocio entre invocaciones, y la cantidad de instancias la decide el tráfico, no tú. La reutilización ocurre solo dentro de una instancia caliente — nunca entre instancias —, así que concurrencia 300 significa 300 pools de una conexión. Subir max_connections compra holgura a un costo real de memoria y pierde contra el siguiente pico más grande.
En un backend gestionado, el código de aplicación se sale de esta pelea por completo — las llamadas de SDK son solicitudes HTTPS sin estado, y ningún cliente retiene jamás una conexión de base de datos:
// JavaScript / Node.js — Back4app JS SDK
// Inside a serverless function: no driver, no pool, no connection to leak.
// Each SDK call is a stateless HTTPS request; pooling happens platform-side.
Parse.initialize('APP_ID', 'JS_KEY');
Parse.serverURL = 'https://parseapi.back4app.com';
export async function handler(event) {
const query = new Parse.Query('Order');
query.equalTo('status', 'pending');
query.limit(20);
const orders = await query.find(); // request returns; nothing stays open
return orders.map((o) => o.id);
}
// 1,000 concurrent invocations = 1,000 HTTP requests,
// not 1,000 database connections. // Flutter / Dart — Back4app Flutter SDK
// No driver, no pool, no connection to leak from the client side.
// Each SDK call is a stateless HTTPS request; pooling happens platform-side.
await Parse().initialize(
'APP_ID',
'https://parseapi.back4app.com',
clientKey: 'CLIENT_KEY',
);
Future<List<String>> pendingOrderIds() async {
final query = QueryBuilder<ParseObject>(ParseObject('Order'))
..whereEqualTo('status', 'pending')
..setLimit(20);
final response = await query.query(); // request returns; nothing stays open
return response.results!
.map((o) => (o as ParseObject).objectId!)
.toList();
}
// A burst of clients = a burst of HTTP requests,
// not a burst of database connections. // iOS / Swift — Back4app Swift SDK
// No driver, no pool, no connection to leak from the client side.
// Each SDK call is a stateless HTTPS request; pooling happens platform-side.
ParseSwift.initialize(
applicationId: "APP_ID",
clientKey: "CLIENT_KEY",
serverURL: URL(string: "https://parseapi.back4app.com")!
)
let query = Order.query("status" == "pending")
.limit(20)
query.find { result in
if case .success(let orders) = result {
render(orders) // request returned; nothing stays open
}
}
// A burst of clients = a burst of HTTP requests,
// not a burst of database connections. // Android / Kotlin — Back4app Android SDK
// No driver, no pool, no connection to leak from the client side.
// Each SDK call is a stateless HTTPS request; pooling happens platform-side.
Parse.initialize(
Parse.Configuration.Builder(context)
.applicationId("APP_ID")
.clientKey("CLIENT_KEY")
.server("https://parseapi.back4app.com")
.build()
)
val query = ParseQuery.getQuery<ParseObject>("Order")
query.whereEqualTo("status", "pending")
query.limit = 20
query.findInBackground { orders, e ->
if (e == null) render(orders) // request returned; nothing stays open
}
// A burst of clients = a burst of HTTP requests,
// not a burst of database connections. Cómo el pooler absorbe el fan-out
Un pooler como PgBouncer acepta conexiones de cliente de a miles — cada una barata de su lado — y presta conexiones reales de servidor desde un pool pequeño solo cuando llega trabajo. La base ve veinte conexiones calmas; quinientas funciones creen tener una cada una. Cuando la demanda excede el pool, los clientes esperan milisegundos en una cola en vez de recibir errores — convirtiendo una falla dura en latencia modesta.
El dimensionamiento del pool es donde la intuición más falla. El paralelismo útil de una base está acotado por núcleos y disco — el rendimiento típicamente alcanza su pico con un pool de unas pocas decenas, cerca del número de núcleos por dos, y luego cae a medida que más conexiones suman contención. La aritmética del pooler funciona porque las solicitudes serverless son cortas: una conexión que atiende una transacción de 8 ms puede atender bastante más de cien por segundo, así que veinte conexiones reales absorben con comodidad miles de invocaciones de función. El pool no es un caché de conveniencia; es un cuello de botella deliberado, colocado donde encolar es barato.
La perilla crucial es cuánto dura el préstamo.
Transaction pooling vs. session pooling
| Dimensión | Session pooling | Transaction pooling |
|---|---|---|
| Duración del préstamo | La sesión entera del cliente | Una transacción, luego se recupera |
| Multiplicador de compartición | Bajo — los clientes ociosos estacionan conexiones | Alto — ideal para trabajo serverless corto |
| Prepared statements | Totalmente soportados | Solo con soporte a nivel de protocolo, configurado explícitamente |
Estado de sesión (SET, tablas temporales, LISTEN/NOTIFY) | Funciona | Se rompe — transacciones distintas pueden caer en conexiones distintas |
| Mejor para | Apps longevas, herramientas de admin, migraciones | Tráfico web y serverless de solicitud/respuesta |
El modo transacción es lo que vuelve viables las cargas serverless — una función que corre una transacción de 8 ms no debería ser dueña de una conexión durante su vida caliente de varios minutos —, pero no es gratis: todo lo que asume que “mi conexión” persiste entre sentencias se convierte en un bug sutil. El consejo consagrado: transaction pooling para el tráfico de solicitudes, y una conexión directa o por sesión para migraciones y cualquier cosa con estado.
Casos de uso comunes
- Funciones serverless y de edge golpeando SQL. El caso canónico — cantidades de instancias irregulares contra un presupuesto fijo de conexiones.
- Muchos servicios pequeños, una base. Veinte servicios × pools por defecto de diez conexiones = 200 conexiones que nadie planificó; un pooler compartido devuelve la cordura a la aritmética.
- Plataformas multi-tenant. Las cargas por tenant multiplican la demanda de conexiones; el pooling mantiene aplicable el presupuesto de la base compartida.
- Ráfagas de tráfico en bases modestas. Los picos del día de lanzamiento se encolan en el pooler por milisegundos en vez de dar error en la base.
- Proteger el primario durante incidentes. Un pool de tamaño fijo es un mamparo: los clientes desbocados agotan la cola del pooler, no la memoria de la base.
¿Deberías operar tu propio pooler? Matriz de decisión
| Opera un pooler cuando… | Sáltatelo cuando… |
|---|---|
| Funciones o muchos servicios conectan directo a SQL | Un BaaS gestionado termina el tráfico del cliente en su capa de API |
| Aparecen errores de conexión bajo picos de carga | Tráfico estable desde pocos servidores longevos con pools en el cliente |
| Tú operas la base y su presupuesto | La plataforma ya hace pooling entre la capa de API y la base |
| Tenants o equipos comparten una base | La carga es una app, un pool, bien dentro de los límites |
| Puedes operar un componente crítico más | Nadie está de guardia para el pooler |
La asimetría honesta: un pooler resuelve el agotamiento de conexiones; un backend gestionado lo disuelve. Cuando los clientes hablan HTTPS con APIs generadas automáticamente, el fan-out nunca llega a la capa de base de datos — el pooling sigue ocurriendo, pero como infraestructura bien probada de alguien más y no como componente tuyo.
Limitaciones y trade-offs
- Un pooler es un componente. Necesita despliegue, monitoreo, failover y upgrades — cambiaste un problema de recursos por un problema de operación, a un tipo de cambio favorable pero no nulo.
- El modo transacción cambia la semántica. Prepared statements, variables de sesión y advisory locks dejan de comportarse como documenta el manual; las fallas son intermitentes y dependientes de la carga — el peor tipo.
- Un salto extra cuesta latencia. Típicamente submilisegundo dentro de la red, pero está en el camino de cada consulta.
- El pooler puede ser el cuello de botella. Un pooler de un solo hilo bajo un pico violento encola profundo; el techo se movió, no desapareció.
- El pooling no arregla consultas lentas. Raciona conexiones; una consulta que retiene su préstamo por segundos mata de hambre al pool. Las consultas rápidas — y los índices detrás de ellas — siguen siendo la palanca real de capacidad.
Pooling de conexiones en Back4app
Back4app es una plataforma open-source de Backend as a Service (BaaS) que combina base de datos gestionada, APIs REST y GraphQL generadas automáticamente, autenticación, almacenamiento de archivos y funciones serverless con Cloud Code. Su arquitectura responde la pregunta del pooling de forma estructural: las apps cliente y las funciones de Cloud Code emiten solicitudes HTTPS sin estado, y la capa de servidor de la plataforma — no tu código — mantiene pools de conexiones longevos y bien dimensionados hacia la base gestionada. El fan-out de instancias del lado del cómputo nunca se traduce en fan-out de conexiones del lado de la base, que es exactamente la propiedad que este artículo entero existe para lograr.
Preguntas frecuentes
¿Por qué el serverless agota las conexiones de la base de datos?
Porque dos modelos de escalado colisionan: el cómputo serverless multiplica instancias por solicitud, mientras la base de datos tiene un presupuesto fijo de conexiones — a menudo alrededor de 100 por defecto. Cada instancia de función abre su propia conexión, la retiene durante el tiempo ocioso, y un pico de tráfico que crea 500 instancias pide 500 conexiones a la vez. La base rechaza el excedente, y consultas sin ninguna relación empiezan a fallar.
¿Qué hace en realidad un pooler como PgBouncer?
Se sitúa entre tu código y la base, acepta miles de conexiones de cliente baratas y las multiplexa sobre un pool pequeño de conexiones reales de servidor. Los clientes creen que tienen una conexión; en realidad toman una prestada por una transacción o sesión y la devuelven. La base ve un conjunto calmo y fijo de conexiones sin importar cuántas funciones se levanten.
¿Cuál es la diferencia entre transaction pooling y session pooling?
La duración del préstamo. El session pooling asigna una conexión real durante toda la sesión del cliente — seguro para cualquier feature, pero un cliente ocioso igual estaciona una conexión. El transaction pooling presta la conexión solo mientras corre una transacción y luego la recupera, lo que multiplica el compartir dramáticamente pero rompe el estado de sesión: prepared statements, variables de sesión y LISTEN/NOTIFY exigen cuidado o fallan.
¿Por qué se disparan las conexiones cuando las funciones escalan?
Una plataforma serverless maneja la concurrencia clonando instancias, y cada clon inicializa su propio cliente de base de datos. La reutilización de conexiones ocurre solo dentro de una instancia caliente, nunca entre instancias. Así que concurrencia 300 significa 300 pools de tamaño uno — la peor forma posible: todo el overhead del pooling sin nada del compartir. El fan-out es estructural, no un bug de tu código.
¿Qué tan grande debe ser un pool de conexiones?
Mucho más pequeño de lo que sugiere la intuición. El rendimiento suele alcanzar su pico con un pool cercano al paralelismo real de la base — una regla práctica común es el número de núcleos por dos, ajustado por esperas de disco —, no en los cientos. Más allá de eso, las conexiones extra agregan contención y costo de memoria sin agregar rendimiento. El trabajo del pooler es precisamente hacer que un pool pequeño se sienta infinito para quien llama.
¿Sigo necesitando un pooler si uso un BaaS?
No para tu propio tráfico — ese es el punto. Un backend gestionado termina las solicitudes del cliente como llamadas HTTPS sin estado en la capa de API; su propia capa de servidor mantiene pools longevos y bien dimensionados hacia la base. Tus funciones y apps nunca retienen conexiones de base de datos, así que el escenario de agotamiento desaparece en vez de mitigarse. Los poolers solo regresan si conectas herramientas externas directo a la base.
¿Cuáles son las desventajas de los poolers de conexiones?
Agregan un salto de red de latencia, se vuelven un componente que debes operar y monitorear, y el modo transacción cambia la semántica en silencio — las features con alcance de sesión dejan de comportarse como documenta el manual. Un solo proceso de pooler puede volverse él mismo el cuello de botella bajo carga irregular, y dimensionarlo mal solo mueve la cola de lugar. Es infraestructura esencial, pero es infraestructura.