PRODPlataforma BaaS soberana europeaAbrir panel →

Rendimiento · 10 lectura mínima

PgBouncer agrupación de transacciones explicada

Affane Daylami · Fondateur · 12 de junio de 2026

volver al blog

El modo de transacción de PgBouncer libera la conexión PostgreSQL al final de cada transacción, no cuando el cliente se desconecta. Esto es lo que hace posible atender a miles de clientes HTTP con unas pocas docenas de conexiones de servidor reales, y es el modo recomendado para cualquier API REST de consulta corta.

Este texto en inglés se generó automáticamente a partir del original en francés y aún no ha sido revisado.
Esta página fue traducida automáticamente. La versión en inglés es autorizada.

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 exist bajo 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_path en cada solicitud.
  • Verificado en el código de Aurabase: los grupos de inquilinos se ejecutan con statement_cache_capacity(0) y PgBouncer en pool_mode=transaction, mientras que PostgREST permanece voluntariamente en conexión directa para la recarga de su esquema a través de LISTEN/NOTIFY.
#
Conceptos

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).

ModaConexión del servidor sueltaCompatibilidad de sesiones
sesión (predeterminado)Sobre la desconexión del clienteTotal: SET, LISTEN, cursores, todo funciona como en vivo
transacciónAl final de cada transacción (COMMIT/ROLLBACK)Parcial: sólo lo que permanece local a la transacción.
declaraciónDespués de cada solicitud individualMí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.

#
Mecanismo

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.

pgbouncer.iniini
[pgbouncer]
listen_port = 5432

; La connexion serveur est libérée dès la fin de chaque transaction
pool_mode = transaction

default_pool_size = 80
max_client_conn = 1000

; Ne pas utiliser avec des sessions stateful (SET, advisory locks, LISTEN/NOTIFY)

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é.

#
Límites

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ÓNLa configuración se aplica a una conexión que se puede reciclar inmediatamente despuésUn parámetro parece olvidarse aleatoriamente entre dos solicitudes
ESCUCHAR / NOTIFICARAsume una conexión persistente para recibir notificaciones.El cliente nunca es notificado, o sólo de forma intermitente.
Bloqueos de aviso de sesiónEl 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 HOLDDebe sobrevivir más allá de la transacción que lo abrió.Error "el cursor no existe" en la siguiente iteración
Tablas temporalesRelacionado con la sesión de Postgres, no con la transacciónLa tabla "desaparece" en la siguiente consulta.
Declaraciones preparadas nombradasPreparado en una conexión de servidor específica, reproducido en otra"declaración preparada... no existe" bajo carga
La trampa no siempre es inmediata

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.

#
Trampa común

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.

pool.rsrust
let connect_options = url
    .parse::<PgConnectOptions>()?
    .statement_cache_capacity(0);

// Equivalentes: asyncpg -> state_cache_size=0, pgjdbc -> prepareThreshold=0

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.

#
Comprobado en código

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).

#
guía practica

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.

  1. Audita el código de la aplicación. Busque SET, LISTEN/NOTIFYque no sean transacciones, bloqueos de aviso de sesión, cursores WITH HOLD y tablas temporales reutilizadas entre consultas.
  2. 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.
  3. 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.
  4. 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.
  5. Tamaño default_pool_size y max_client_conn relativo al max_connectionsreal de Postgres, no por una cifra arbitraria copiada de otro proyecto.
  6. 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.
  7. Supervise SHOW POOLS y SHOW STATS desde 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.
#
decisión

¿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

Preguntas frecuentes

Las preguntas que surgen con más frecuencia una vez que se activa el modo de transacción en producción.

¿Qué es el modo de agrupación de transacciones de PgBouncer?+
Este es uno de los 3 modos de PgBouncer (con sesión y declaración) en el que la conexión de Postgres se reasigna a otro cliente al final de cada transacción, en lugar de cuando el cliente se desconecta. Esto hace posible atender a muchos más clientes competidores que conexiones abiertas de Postgres.
¿Por qué mis estados de cuenta preparados fallan en el modo de transacción?+
Se prepara una declaración preparada con nombre en una conexión de servidor específica. En modo transacción, esta conexión se puede reasignar a otro cliente entre dos solicitudes. Si su controlador reproduce el nombre de la declaración en una conexión donde nunca se preparó, Postgres devuelve un error como que la declaración preparada no existe. La corrección consiste en deshabilitar el caché de declaraciones preparadas del lado del controlador (statement_cache_capacity(0) con sqlx, Statement_cache_size=0 con asyncpg).
¿Podemos usar LISTEN/NOTIFY detrás de un PgBouncer en modo de transacción?+
No, no de manera confiable. LISTEN/NOTIFY asume una conexión persistente para recibir notificaciones, lo cual el modo de transacción no garantiza. La práctica estándar es pasar componentes que dependen de LISTEN/NOTIFY (PostgREST, por ejemplo) a través de una conexión directa a Postgres, fuera del pooler.
¿Deberíamos utilizar SET LOCAL en lugar de SET en modo transacción?+
Sí, sistemáticamente para cualquier configuración que deba aplicarse a una solicitud determinada. SET LOCAL se limpia automáticamente en COMMIT o ROLLBACK, lo que lo hace seguro con una conexión de servidor que puede cambiar entre dos transacciones. Un SET clásico puede filtrarse al siguiente cliente que recupera la misma conexión de servidor reciclada.
¿El modo de transacción funciona con la seguridad de nivel de fila (RLS)?+
Sí, siempre que las notificaciones JWT o las variables de sesión utilizadas por sus políticas RLS estén configuradas en LOCAL SET dentro de la transacción, no en el SET de sesión. Este es el patrón descrito en nuestro artículo sobre el aislamiento RLS multiinquilino.
PgBouncer, Supavisor, PgCat: ¿cuál elegir?+
Los tres implementan un modelo de agrupación similar, con diferencias en la agrupación, la distribución de carga y el ecosistema (Supavisor está desarrollado por Supabase, PgCat está escrito en Rust). La elección depende sobre todo de su topología de implementación y de sus limitaciones operativas existentes: consulte nuestra comparación dedicada para obtener más detalles.

¿LISTO PARA IMPLEMENTAR?

Tu backend en cinco minutos.

No se requiere tarjeta de crédito · 500 MB gratis · 50,000 MAU