Esta ganancia tiene un costo específico: el modo de transacción rompe silenciosamente todo lo que supone una conexión Postgres estable de una solicitud a la siguiente. Sesión SET, LISTEN/NOTIFY, bloqueos de aviso, cursores que sobreviven a la transacción, declaraciones preparadas con nombre. Este artículo detalla el mecanismo, enumera estos límites con sus síntomas exactos y luego muestra cómo un backend en producción (el nuestro, verificado directamente en su repositorio) lo configura sin quedar atrapado por él. Para conocer el método de medición detrás de las cifras de rendimiento citadas aquí, consulte nuestra metodología de referencia .
Lo esencial
- Modo de transacción: la conexión del servidor se libera al final de cada transacción, no cuando el cliente se desconecta. Este es el modo más eficiente para compartir conexiones cortas de tipo API REST.
- Incompatible por construcción con: sesión SET/RESET, LISTEN/NOTIFY, bloqueos de aviso de sesión, cursores CON HOLD, tablas temporales reutilizadas de una solicitud a otra.
- El error más común en la práctica: las declaraciones preparadas con nombre, que varios controladores (sqlx, asyncpg, el controlador JDBC pgjdbc) activan de forma predeterminada, pueden reproducirse en una conexión de servidor diferente y desencadenar un error como
prepared statement does not existbajo carga. - Desde la versión 1.21, PgBouncer puede seguir declaraciones preparadas por el protocolo en modo de transacción (caché LRU a través de una conexión al servidor). Esto no impide deshabilitar el caché del lado del cliente si su aplicación cambia
search_pathen cada solicitud. - Verificado en el código de Aurabase: los grupos de inquilinos se ejecutan con
statement_cache_capacity(0)y PgBouncer enpool_mode=transaction, mientras que PostgREST permanece voluntariamente en conexión directa para la recarga de su esquema a través de LISTEN/NOTIFY.
Los 3 modos de agrupación de PgBouncer
PgBouncer ofrece tres modos, que difieren sólo en cuando la conexión del servidor Postgres regresa al grupo común. La documentación oficial los nombra session, transaction y statement (pgbouncer.org/features.html, sección “Modos de agrupación”, consultada el 24 de agosto de 2026).
| Moda | Conexión del servidor suelta | Compatibilidad de sesiones |
|---|---|---|
| sesión (predeterminado) | Sobre la desconexión del cliente | Total: SET, LISTEN, cursores, todo funciona como en vivo |
| transacción | Al final de cada transacción (COMMIT/ROLLBACK) | Parcial: sólo lo que permanece local a la transacción. |
| declaración | Después de cada solicitud individual | Mínimo: transacciones explícitas de consultas múltiples prohibidas |
El modo de sesión es el más permisivo pero el menos efectivo en escalabilidad: una conexión Postgres permanece reservada para un cliente mientras permanezca conectado, incluso si no hace nada entre dos solicitudes. El modo de declaración está reservado para casos muy específicos (proxy de solo lectura, controles de estado) e incluso interrumpe las transacciones explícitas clásicas. El modo de transacción es el compromiso que domina en la práctica para una API REST: cada solicitud HTTP generalmente corresponde a una única transacción corta de Postgres.
Cómo funciona el modo transacción, conexión por conexión
En modo de transacción, PgBouncer solo conecta una conexión de servidor a un cliente cuando este último abre una transacción y la devuelve al grupo al COMMIT o ROLLBACK. Entre dos transacciones, el mismo cliente puede verse reasignado a una conexión de servidor completamente diferente.
En concreto, con un default_pool_size de 20, PgBouncer puede absorber varios cientos de clientes simultáneos que, en un momento dado, sólo tienen unas pocas transacciones en curso. Es esta relación la que justifica el modo de transacción para una API REST con mucho tráfico pero transacciones cortas: el recurso escaso (una conexión Postgres, costosa en memoria en el lado del servidor) sólo se ocupa durante el tiempo estrictamente necesario.
Este último comentario resume lo esencial: el modo de transacción funciona porque rompe deliberadamente el vínculo entre "mi sesión de aplicación" y "mi conexión Postgres". Todo lo que se basa en este vínculo se rompe. La siguiente sección enumera exactamente qué.
Qué se rompe en el modo de agrupación de transacciones
La documentación oficial de PgBouncer enumera explícitamente las características de PostgreSQL que pierden su significado tan pronto como se puede reciclar una conexión de servidor entre dos solicitudes del mismo cliente.
| Funcionalidad afectada | ¿Por qué se rompe? | Síntoma típico |
|---|---|---|
| ESTABLECER / ESTABLECER SESIÓN | La configuración se aplica a una conexión que se puede reciclar inmediatamente después | Un parámetro parece olvidarse aleatoriamente entre dos solicitudes |
| ESCUCHAR / NOTIFICAR | Asume una conexión persistente para recibir notificaciones. | El cliente nunca es notificado, o sólo de forma intermitente. |
| Bloqueos de aviso de sesión | El bloqueo lo mantiene la conexión del servidor, no el cliente lógico. | Un bloqueo se libera antes de la finalización esperada, o nunca se libera |
| CON controles deslizantes HOLD | Debe sobrevivir más allá de la transacción que lo abrió. | Error "el cursor no existe" en la siguiente iteración |
| Tablas temporales | Relacionado con la sesión de Postgres, no con la transacción | La tabla "desaparece" en la siguiente consulta. |
| Declaraciones preparadas nombradas | Preparado en una conexión de servidor específica, reproducido en otra | "declaración preparada... no existe" bajo carga |
La mayoría de estas limitaciones no se manifiestan en el desarrollo local, donde una única conexión normalmente sirve a todo el tráfico. Aparecen bajo carga real, cuando varios clientes comparten el grupo y una conexión de servidor cambia de manos entre dos solicitudes del mismo cliente lógico. Una prueba de humo casi nunca los revela.
Declaraciones preparadas: el límite más incomprendido
La mayoría de los controladores Postgres modernos preparan solicitudes con nombre en el lado del protocolo de forma predeterminada, sin que el código de la aplicación lo solicite explícitamente. Esto es precisamente lo que hace que esta trampa sea difícil de anticipar.
Una declaración preparada por el protocolo se nombra y se almacena en caché en una conexión de servidor específica, en el momento de Parse. En modo transacción, esta conexión se puede reasignar a otro cliente entre dos solicitudes del mismo cliente lógico. Si el controlador luego reproduce el mismo nombre de declaración en una conexión donde nunca se preparó, Postgres responde con un error explícito, generalmente prepared statement "sqlx_s_N" does not exist para un cliente sqlx. El comportamiento es intermitente: depende de cómo funcionan las conexiones bajo carga, no de un error determinista reproducible en cada llamada.
La corrección del lado del cliente es la misma independientemente del idioma: deshabilite el caché de declaraciones preparadas con nombre o fuerce consultas sin nombre para cualquier grupo que cruce un grupo en modo de transacción. En Rust con sqlx, pasa por statement_cache_capacity(0) en las opciones de conexión.
Desde la versión 1.21, PgBouncer alivia parte del problema en el lado del servidor: puede seguir declaraciones preparadas por el protocolo en modo transacción y prepararlas sobre la marcha en la conexión asignada, con un caché LRU por conexión cuyo tamaño se ajusta mediante max_prepared_statements. Esto reduce la cantidad de errores, pero no lo exime de deshabilitar el caché del cliente en un grupo de múltiples inquilinos donde search_path cambia de una solicitud a otra: un plan almacenado en caché congela el identificador interno (OID) de la tabla resuelta en el momento de Parsey reproducirlo bajo otro esquema puede devolver datos del inquilino incorrecto en lugar de un simple error.
Cómo Aurabase configura PgBouncer en modo transacción
El repositorio de Aurabase implementa PgBouncer como pool_mode=transaction frente al plano de datos compartido (deploy/helm/aurabase/templates/infra/pgbouncer.yaml) y un Pooler CNPG configurado de manera idéntica frente a cada instancia de Postgres dedicada de un inquilino (deploy/cnpg/tenant-pooler.yaml). Ambos caminos aplican la misma disciplina descrita anteriormente.
El código fuente documenta un motivo de seguridad específico para esta elección, no sólo un motivo de estabilidad. Los grupos de Postgres compartidos entre inquilinos colocan un search_path diferente por proyecto en conexiones reutilizadas. Una declaración preparada almacenada en caché congela el OID de la tabla resuelta en el momento de Parse; reproducirlo para otro inquilino en la misma conexión ejecutaría la consulta en el esquema del primer inquilino, una omisión de aislamiento, no solo un error de aplicación. Por lo tanto, statement_cache_capacity(0) se aplica sin excepción, incluso en instancias dedicadas que también pasan por un CNPG Pooler en modo de transacción.
Segunda medida de higiene de la sesión: cuando cada conexión regresa al grupo, un enlace ejecuta DISCARD ALL (restableciendo la configuración, desasignando declaraciones preparadas en el lado del servidor, liberando bloqueos de aviso, purgando cursores y tablas temporales). Sin este enlace, un residuo de sesión planteado por una solicitud anterior podría filtrarse en la siguiente solicitud de un inquilino diferente que reutilice la misma conexión reciclada.
Excepción aceptada: las instancias dedicadas de PostgREST permanecen en conexión directa con la primaria, sin pasar por el pooler. La recarga del esquema PostgREST se basa en LISTEN/NOTIFY, que supone una conexión persistente, exactamente la funcionalidad que interrumpe el modo de transacción (detalle ya documentado en nuestro artículo sobre Compatibilidad con PostgREST en Aurabase). La configuración de RLS por solicitud se pasa a SET LOCAL dentro de una transacción explícita, la única forma de seguir siendo compatible con un grupo que puede cambiar las conexiones del servidor en cualquier COMMIT (consulte nuestro artículo sobreaislamiento de RLS multiinquilino).
Habilite el modo de transacción sin interrumpir su aplicación
Una breve lista de verificación, aplicable a cualquier backend que pase de una conexión directa de Postgres a un PgBouncer en modo de transacción.
- Audita el código de la aplicación. Busque
SET,LISTEN/NOTIFYque no sean transacciones, bloqueos de aviso de sesión, cursoresWITH HOLDy tablas temporales reutilizadas entre consultas. - Reemplace los SET de sesión con SET LOCALES dentro de una transacción explícita. Esta es la única configuración que sobrevive adecuadamente al reciclaje de la conexión, porque se limpia en COMMIT/ROLLBACK en lugar de filtrarse en la siguiente conexión.
- Deshabilite el caché de declaraciones preparadas del lado del controlador si su grupo atraviesa el grupo y el esquema o función cambia de una solicitud a otra. El costo del desempeño es real pero mensurable y mucho menor que el riesgo de fugas entre inquilinos.
- Aísle las conexiones que realmente necesitan el modo de sesión (migraciones, scripts de administración, cualquier cosa que dependa de LISTEN/NOTIFY) a una conexión directa que no sea de agrupación, en lugar de renunciar al modo de transacción para el resto del tráfico.
- Tamaño
default_pool_sizeymax_client_connrelativo almax_connectionsreal de Postgres, no por una cifra arbitraria copiada de otro proyecto. - Prueba bajo carga real, no solo prueba de humo. Los errores de declaraciones preparadas y las fugas de configuración de sesión casi nunca aparecen en una sola conexión local.
- Supervise
SHOW POOLSySHOW STATSdesde la consola de administración de PgBouncer una vez en producción, para detectar la saturación del grupo antes de que sea visible en el lado del cliente.
¿Debería elegir siempre el modo de transacción en lugar de la sesión?
No, pero es la opción predeterminada correcta para la gran mayoría de las API REST. El modo de sesión sigue siendo preferible para una aplicación heredada que depende en gran medida de funcionalidades de sesión que no se pueden refactorizar rápidamente, o para tráfico bajo donde la ganancia de la agrupación no compensa el esfuerzo de migración.
PgBouncer tampoco es la única implementación de este modelo de agrupación: Supavisor (Supabase) y PgCat son dos alternativas recientes, con diferentes compensaciones en cuanto a distribución de carga y agrupación. Vea nuestra comparación detallada, PgBouncer vs Supavisor vs PgCat, para elegir entre los tres según su topología.
Preguntas frecuentes
Las preguntas que surgen con más frecuencia una vez que se activa el modo de transacción en producción.