Pool de conexiones PostgreSQL en un VPS de 4 GB
Cada conexión de PostgreSQL es un proceso con memoria propia: un VPS de 4 GB puede quedarse sin RAM antes de max_connections. Qué arregla y qué rompe un pooler.
Por qué un VPS pequeño se queda sin RAM antes de alcanzar max_connections
La agrupación de conexiones de Postgres en un VPS no es un truco para aumentar la velocidad. Es lo que mantiene operativo un servidor de 4 GB, porque cada conexión de PostgreSQL es un proceso independiente del sistema operativo que mantiene su propia memoria privada. Un pooler coloca un número pequeño y fijo de procesos backend reales detrás de un número grande y económico de conexiones de cliente.
El valor predeterminado de max_connections es 100. Es un límite, no un presupuesto. PostgreSQL nunca comprueba si la máquina puede mantener realmente 100 backends ejecutando consultas, por lo que la máquina falla primero. El asesino de procesos por falta de memoria (OOM) del kernel selecciona un proceso y, cuando selecciona un backend, PostgreSQL reinicia todo el clúster para proteger la memoria compartida. El registro muestra server process (PID 1234) was terminated by signal 9: Killed y después terminating any other active server processes. Todas las conexiones abiertas terminan, incluidas las que estaban funcionando correctamente.
El servidor se queda sin memoria porque cada conexión es un proceso y porque work_mem se asigna para cada operación de ordenación o de hash, no para cada conexión. Ambos factores se multiplican.
Cada conexión es un proceso y cada proceso consume memoria
PostgreSQL usa un proceso por conexión. El proceso postmaster crea un backend cuando se conecta un cliente, y ese backend permanece activo hasta que el cliente se desconecta. No es un thread. Tiene sus propias tablas de páginas, sus propias cachés de catálogos y sus propios planes de consulta en caché. Esas cachés crecen a medida que la conexión accede a más tablas y ejecuta más consultas distintas. Por eso, una conexión de larga duración en una aplicación ORM con mucha actividad consume más que una conexión nueva.
La memoria compartida se comparte realmente. shared_buffers es una asignación para todo el clúster y se asigna en cada backend. La memoria privada no se comparte. Por eso top puede inducirle a error: el tamaño del conjunto residente (RSS) de un backend incluye las páginas compartidas que ese backend ha tocado. Si suma el RSS de 50 backends, cuenta shared_buffers 50 veces.
Mida en su lugar la parte privada. PSS (proportional set size) divide cada página compartida entre el número de procesos que la asignan. USS (unique set size) cuenta sólo las páginas que pertenecen exclusivamente a ese proceso.
sudo apt update
sudo apt install -y smem
sudo smem -k -P '^postgres'La columna USS muestra la memoria que se liberaría si terminara ese backend. Ese es el coste real de cada conexión. Las cifras publicadas suelen situar un backend inactivo en unos pocos megabytes y un backend que ha ejecutado consultas ORM amplias en varias veces esa cantidad. Considere esas cifras como valores publicados habituales, no como sus propios valores. La única cifra útil para planificar es la de su servidor con su carga de trabajo.
Es fácil pasar por alto una asignación por sesión. temp_buffers tiene un valor predeterminado de 8MB y se asigna por sesión la primera vez que esa sesión accede a una tabla temporal. No se devuelve hasta que termina la sesión.
work_mem se asigna por operación, no por conexión
Aquí es donde los cálculos suelen inducir a error. work_mem tiene un valor predeterminado de 4MB, y la documentación de PostgreSQL lo explica claramente: "una consulta compleja puede realizar varias operaciones de ordenación y hash al mismo tiempo, y cada operación suele poder utilizar tanta memoria como especifica este valor antes de empezar a escribir datos en archivos temporales". Un plan con tres nodos de ordenación puede utilizar tres veces work_mem dentro de un mismo backend y en el mismo momento.
Las operaciones hash pueden utilizar más memoria. hash_mem_multiplier tiene un valor predeterminado de 2.0, por lo que una combinación hash o una agregación hash puede utilizar work_mem multiplicado por dos, es decir, 8MB con la configuración predeterminada. Las consultas paralelas vuelven a multiplicar el consumo, porque cada worker paralelo es otro proceso con su propia asignación.
Haga los cálculos para una VPS de 4 GB. Establezca shared_buffers en 1 GB, mantenga work_mem en 4MB y permita que 100 conexiones ejecuten cada una una consulta con dos nodos hash. Eso equivale a 100 por 16MB, es decir, 1.6 GB de memoria privada además de 1 GB de buffers compartidos, sin contar la caché de páginas ni el resto de procesos del sistema. Ahora aumente work_mem a 64MB porque el servidor tiene memoria disponible, y esas mismas 100 conexiones representan 100 por 256MB. No recibirá ninguna advertencia. Lo descubrirá cuando actúe el OOM killer.
Puede comprobar si work_mem es demasiado pequeño en lugar de adivinar. Establezca log_temp_files = 0 en postgresql.conf y recargue la configuración. Cada escritura temporal en disco registrará entonces una línea con el nombre y el tamaño del archivo, como temporary file: path "base/pgsql_tmp/pgsql_tmp1234.0", size 20971520. Las escrituras temporales frecuentes indican que un valor mayor de work_mem sería útil. Si no hay escrituras temporales, aumentarlo no aporta nada y consume memoria que no tiene.
La aritmética del pool que realmente causa problemas
Nadie pretende abrir 240 conexiones. Configura un pool de 20 conexiones y después ejecuta la aplicación en más de un lugar.
The data behind this chart
[
{
"config": "1 worker",
"backends": 20
},
{
"config": "4 web workers",
"backends": 80
},
{
"config": "4 web + 2 background",
"backends": 120
},
{
"config": "2 hosts x 4 workers",
"backends": 160
},
{
"config": "3 hosts x 4 workers",
"backends": 240
}
]Cuatro workers de Gunicorn, cada uno con un pool de 20 conexiones, solicitan 80 backends. Si añade dos workers para tareas en segundo plano, son 120. Si aumenta hasta 3 hosts x 4 workers, la aplicación solicita 240 backends frente a un max_connections de 100. Ninguna de esas 5 configuraciones está mal configurada por separado. El pool es independiente para cada proceso y ninguna parte de la aplicación puede ver el total.
Los valores predeterminados de las bibliotecas apuntan en la misma dirección. El valor predeterminado de QueuePool de SQLAlchemy es pool_size=5 con max_overflow=10, es decir, 15 conexiones por proceso. HikariCP tiene un valor predeterminado de 10. Django no tenía un pool integrado antes de 5.1 y utilizaba una conexión por proceso worker. Por eso las aplicaciones Django se encuentran con este problema más tarde y, después, de forma repentina, cuando alguien configura CONN_MAX_AGE o activa la nueva opción "pool": True. Si ejecuta una aplicación Django detrás de Gunicorn y nginx, debe multiplicar el número de workers de Gunicorn, no el número de servidores.
Qué cambia realmente el connection pooling de Postgres en un VPS
Un pooler es un proceso que habla el protocolo wire de PostgreSQL con la aplicación por un lado y mantiene un conjunto pequeño de conexiones reales al servidor por el otro. No acelera ninguna consulta. Cambia quién asume el coste de una conexión y cuántos backends reales existen.
Mejoran dos aspectos. Abrir una conexión deja de costar un fork y las búsquedas en el catálogo que llenan la caché vacía de un backend, porque el pooler responde directamente al connect del cliente. Más importante aún, el número de backends reales deja de seguir el número de conexiones de la aplicación, de modo que 500 clientes pueden compartir 20 backends.
La espera es la funcionalidad, y esta es la parte que suele generar rechazo. Sin un pooler, 500 consultas simultáneas obtienen un backend y se ejecutan a la vez en 2 núcleos de CPU, por lo que todas son lentas y toda la memoria se consume en el mismo momento. Con un pooler, se ejecutan 20 y el resto espera unos milisegundos. Así, cada consulta en ejecución obtiene una parte real de la CPU y termina antes. Una cola delante de un pool pequeño es mejor que no tener cola delante de uno grande.
Un pooler no limita nada más en la máquina. Si Postgres comparte el VPS con un servidor de aplicaciones o con una base de datos vectorial en el mismo VPS, el pooler protege Postgres de la aplicación y nada más. Establezca también un límite estricto para los servicios vecinos: puede limitar la memoria y la CPU que puede usar un servicio con systemd para que un proceso descontrolado no arrastre la base de datos. El lugar donde se ejecuta la base de datos determina cómo se configuran esos límites. Esta es la diferencia práctica entre ejecutar Postgres en Docker o directamente en el host.
Agrupación de sesiones frente a agrupación de transacciones
Un ajuste determina todo lo demás: pool_mode.
En la agrupación de sesiones, una conexión del servidor se asigna a un cliente durante toda la vida de esa conexión y se libera cuando el cliente se desconecta. Todo funciona porque el pooler actúa como un proxy simple. Se ahorra el coste de establecer conexiones, pero nada más. Si la aplicación abre 200 conexiones, todavía necesita 200 backends.
En la agrupación de transacciones, una conexión del servidor se asigna a un cliente sólo durante una transacción. En COMMIT o ROLLBACK vuelve al pool y el siguiente cliente en espera la obtiene. Esto es lo que convierte 500 clientes en 20 backends. También es lo que rompe ciertas funciones, y lo hace de forma intencionada: la siguiente sentencia puede ejecutarse en un backend distinto del que ejecutó la anterior.
El valor predeterminado de PgBouncer es pool_mode = session. Instálelo, no cambie nada y obtendrá la parte económica sin ninguna de sus ventajas. Un tercer modo, statement, devuelve la conexión después de cada sentencia y rechaza las transacciones con varias sentencias. No lo cambie a menos que sepa exactamente por qué lo necesita.
Qué se rompe en el modo de transacción y por qué
Todo lo siguiente falla por el mismo motivo. Es estado que vive dentro de un único backend, y el agrupamiento por transacción no garantiza que se use el mismo backend dos veces.
SETyRESETen el nivel de sesión.SET search_path,SET statement_timeout,SET TIME ZONEySET ROLEse ejecutan en el backend que atienda esa sentencia y desaparecen antes de la siguiente transacción. UseSET LOCALdentro de una transacción explícita. Su alcance se limita a esa transacción y, por tanto, es seguro.LISTEN. La entrega de notificaciones pertenece al backend que ejecutóLISTEN, y ese backend se asigna a otro cliente en cuanto termina la transacción.NOTIFYsigue funcionando en el modo de transacción, lo que hace que este fallo sea confuso: el envío funciona, pero la recepción nunca se produce. Si necesitaLISTEN, abra una conexión adicional directamente al puerto 5432 para omitir el pooler.- Bloqueos de asesoría en el nivel de sesión.
pg_advisory_lock()permanece retenido por la sesión y se libera cuando termina la sesión. Con el agrupamiento por transacción, la llamada para liberar el bloqueo se ejecuta en otro backend, por lo que el bloqueo permanece retenido hasta que PgBouncer retire esa conexión del servidor. De forma predeterminada, esto ocurre después deserver_lifetime, una hora. Usepg_advisory_xact_lock(), que se libera al final de la transacción mediante el mismo backend que lo adquirió. PREPAREyDEALLOCATE, las sentencias SQL. Nunca están disponibles en el modo de transacción.- Cursores
WITH HOLDy cualquier cursor del servidor que deba sobrevivir a su transacción. - Tablas temporales que deban sobrevivir a un commit.
CREATE TEMP TABLE ... ON COMMIT PRESERVE ROWScoloca la tabla en el esquema temporal de un backend, y la siguiente transacción puede ejecutarse en otro backend. LOAD.
Las sentencias preparadas en el nivel de protocolo son la excepción que ha cambiado. PgBouncer 1.21.0 añadió compatibilidad con ellas en el modo de transacción, y 1.24.0 la activó de forma predeterminada al establecer max_prepared_statements en 200. Las versiones anteriores dejan ese valor en 0, lo que significa que la función está desactivada. Ubuntu 24.04 incluye PgBouncer 1.22.0, por lo que la función está disponible, pero debe establecer max_prepared_statements manualmente. Si no está seguro de la configuración de su versión, la opción segura está en el cliente: psycopg 3 deja de usar sentencias preparadas en el servidor cuando establece prepare_threshold en None.
Django tiene su propia versión de este problema. La documentación indica que «usar un pooler de conexiones en modo de agrupamiento por transacción (por ejemplo, PgBouncer) requiere desactivar los cursores del servidor para esa conexión», porque «los cursores del servidor sólo están disponibles en la conexión en la que se crearon». Establezca DISABLE_SERVER_SIDE_CURSORS en True en la entrada de esa base de datos, o cada llamada a .iterator() fallará de forma intermitente y el problema sólo aparecerá bajo carga.
El modo de transacción merece la pena, pero implica un contrato. Lea la lista, compruebe que su ORM y su biblioteca de tareas en segundo plano sean compatibles con ella y, después, cámbiese a este modo.
Instalar PgBouncer y dirigir la aplicación hacia él
La configuración siguiente se ejecuta en el servidor. El sistema es Ubuntu 24.04 y PostgreSQL ya está escuchando en 127.0.0.1, puerto 5432.
sudo apt update
sudo apt install -y pgbouncer
pgbouncer --versionUbuntu 24.04 incluye PgBouncer 1.22.0. La versión upstream es 1.25.2 en agosto de 2026. Compruebe qué versión tiene, porque el comportamiento de las sentencias preparadas depende de ella.
Cree un rol cuya única función sea iniciar sesión en la consola administrativa de PgBouncer y, después, genere el archivo de contraseñas. PgBouncer necesita los secretos SCRAM (mecanismo de autenticación mediante respuesta a desafío con salt) de pg_authid, y sólo un superusuario puede leer esa tabla.
sudo -u postgres psql -c "CREATE ROLE pgb_admin LOGIN PASSWORD 'change-this'"
sudo -u postgres psql -At -c \
'SELECT format($$"%s" "%s"$$, rolname, rolpassword) FROM pg_authid WHERE rolpassword IS NOT NULL' \
> /tmp/userlist.txt
sudo install -o postgres -g postgres -m 640 /tmp/userlist.txt /etc/pgbouncer/userlist.txt
rm /tmp/userlist.txtCopiar los secretos, en lugar de volver a escribir las contraseñas, es lo que permite que esto funcione. PgBouncer sólo puede reutilizar un secreto SCRAM para iniciar sesión en PostgreSQL cuando el cliente también se autenticó con SCRAM, cuando el secreto del archivo coincide byte por byte con el de pg_authid (el mismo salt y el mismo número de iteraciones, no sólo la misma contraseña) y cuando la línea [databases] no fija un user=. Añada user=appuser a esa línea para que PgBouncer necesite una contraseña en texto plano. Confirme que el propietario del archivo coincide con la cuenta con la que se ejecuta el servicio mediante systemctl show pgbouncer -p User. Cambiar una contraseña en PostgreSQL implica volver a generar este archivo; de lo contrario, la siguiente conexión devuelve password authentication failed.
Ahora escriba /etc/pgbouncer/pgbouncer.ini.
[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
admin_users = pgb_admin
pool_mode = transaction
max_client_conn = 500
default_pool_size = 20
min_pool_size = 5
max_db_connections = 80
max_prepared_statements = 200
ignore_startup_parameters = extra_float_digitslisten_addr = 127.0.0.1 mantiene el pooler fuera de Internet público. Esto es importante porque un pooler accesible desde el exterior es un endpoint de autenticación que no pretendía publicar. max_client_conn indica cuántas conexiones de la aplicación aceptará PgBouncer; como su coste es bajo, puede ser alto. default_pool_size indica cuántos backends reales puede mantener un par de base de datos y usuario; este es el valor costoso. max_db_connections limita la base de datos completa a 80 conexiones y deja margen por debajo de max_connections para psql, las copias de seguridad y la monitorización. ignore_startup_parameters = extra_float_digits evita que PgBouncer rechace controladores, incluido el controlador JDBC, que envían ese parámetro al conectarse.
sudo systemctl restart pgbouncer
sudo systemctl status pgbouncer --no-pager
sudo journalctl -u pgbouncer -n 20 --no-pagerUn arranque correcto registra una línea que indica que PgBouncer está escuchando en 127.0.0.1:6432. Si el arranque falla por el archivo de contraseñas, el registro muestra la ruta que no pudo leer. Casi siempre se trata de un problema de permisos o propietario, no de sintaxis. Después, cambie la cadena de conexión de la aplicación del puerto 5432 al puerto 6432 y reiníciela. No es necesario cambiar nada más en la aplicación.
Cómo comprobar que el pool cumple su función
PgBouncer tiene una consola de administración a la que se accede mediante una base de datos virtual llamada pgbouncer.
psql -h 127.0.0.1 -p 6432 -U pgb_admin pgbouncerSHOW POOLS;
SHOW STATS;
SHOW CLIENTS;SHOW POOLS es el valor que debe vigilar. cl_active son los clientes conectados actualmente a una conexión del servidor; cl_waiting son los clientes en espera de una conexión; sv_active y sv_idle son los backends reales en uso y libres; y maxwait es el tiempo, en segundos, que lleva esperando el cliente situado al frente de la cola. En condiciones normales de carga, un estado saludable significa que cl_waiting está en 0 y maxwait está en 0. Un maxwait que supera uno o dos segundos indica que el pool es demasiado pequeño o que las consultas son demasiado lentas. Cada caso requiere una solución distinta.
Compruebe cuál de los dos casos se aplica antes de aumentar default_pool_size.
SELECT state, count(*) FROM pg_stat_activity
WHERE backend_type = 'client backend' GROUP BY state;Si la mayoría de los backends permanecen en idle in transaction, el tamaño del pool no es el problema. La aplicación abre una transacción y después ejecuta alguna operación lenta dentro de ella, como una llamada HTTP, por lo que cada backend queda ocupado sin ejecutar una consulta. idle_in_transaction_session_timeout puede interrumpir esas operaciones, pero la solución real está en el código de la aplicación. Si, por el contrario, todos los backends están en active, el pool está realmente saturado y conviene revisar las consultas con EXPLAIN (ANALYZE, BUFFERS) antes de asignar más conexiones.
Para dimensionar el pool, el punto de partida más citado es la heurística de HikariCP: aproximadamente el doble del número de núcleos más uno. En una VPS de 2 núcleos, el resultado es 5. Trate ese valor como una referencia inicial publicada, establezca default_pool_size cerca de él y ajústelo según maxwait. Los pools pequeños pueden parecer inadecuados, pero normalmente ofrecen mejores mediciones, porque un backend en espera no consume recursos, mientras que uno en ejecución consume CPU y memoria y genera contención de bloqueos.
Elegir entre PgBouncer, PgDog y Pgpool-II
PgBouncer es la opción adecuada para el caso habitual: un servidor PostgreSQL, un VPS y una aplicación que abre más conexiones de las que el sistema puede mantener. Hace una sola tarea, su configuración está en un único archivo ini y Debian y Ubuntu lo incluyen en sus paquetes. Gestiona las conexiones en un solo hilo, lo que es suficiente para la carga de trabajo de un VPS y sólo se convierte en un límite en máquinas mucho más grandes.
PgDog resulta interesante cuando la decisión de enrutamiento debe tomarse en el mismo salto de red que el pooling. Se describe como un proxy para escalar PostgreSQL, está escrito en Rust y ofrece pooling de transacciones y sesiones, además de separar lecturas y escrituras mediante el análisis de las consultas. También admite sharding con enrutamiento entre varios shards y confirmación en dos fases. Úselo cuando tenga un primario y una o más réplicas y quiera enviar las lecturas a las réplicas sin que la aplicación tenga que saber que existen. Hay dos aspectos que debe tener en cuenta. Usa AGPLv3, por lo que la cláusula de uso en red es una cuestión de licencia que debe resolver con la persona responsable de esa decisión en su empresa antes de ponerlo en producción; la postura del propio proyecto es que el uso interno y las modificaciones privadas no crean una obligación de publicar el código fuente. Además, es un proyecto reciente, con versiones 0.x y publicaciones semanales, así que fije una etiqueta de versión en lugar de seguir main.
git clone https://github.com/pgdogdev/pgdog
cd pgdog
cargo build --release
./target/release/pgdog --config pgdog.toml --users users.tomlPara compilar desde el código fuente necesita una cadena de herramientas Rust estable y actual, CMake y un compilador de C/C++. También hay binarios Linux precompilados y paquetes Debian en la página de releases, además de una imagen de contenedor en ghcr.io/pgdogdev/pgdog. La configuración se divide en dos archivos. El primero contiene la configuración general y una entrada por base de datos. Aquí se escribe como un array TOML de tablas en línea para distinguir fácilmente ambas formas.
databases = [
{ name = "appdb", host = "127.0.0.1" },
]
[general]
port = 6432
default_pool_size = 10El segundo contiene una entrada por usuario, con el mismo formato de array.
users = [
{ name = "appuser", database = "appdb", password = "change-this" },
]PgDog escucha en 6432 de forma predeterminada, el mismo puerto que PgBouncer, por lo que ambos no pueden usar el valor predeterminado en el mismo host.
Pgpool-II, en la versión 4.7.2 a fecha de junio de 2026, ofrece pooling y balanceo de carga, además de un watchdog para la conmutación por error automática. Estas funciones adicionales introducen más modos de fallo, y debe entender su modelo de pooling antes de elegirlo. Pgpool-II crea mediante pre-fork num_init_children procesos hijo, y cada proceso almacena en caché hasta max_pool conexiones al servidor, por lo que el límite de backends es num_init_children multiplicado por max_pool. Cada proceso hijo atiende a un cliente cada vez, así que el número de clientes que puede aceptar es igual a num_init_children y queda fijado durante el arranque. Además, un cliente inactivo sigue ocupando un proceso hijo. Configure num_init_children en 100 y max_pool en 4 y habrá autorizado 400 backends, que es exactamente el problema que instaló un pooler para resolver. Elija Pgpool-II cuando necesite su conmutación por error y su enrutamiento de consultas, y haga entonces esa multiplicación con cuidado. Si sólo quiere reducir el número de backends, ofrece más componentes de los necesarios para esa tarea.
La cuestión del proxy gestionado y su equivalente autohospedado
Las plataformas gestionadas lo ofrecen como un producto independiente. AWS coloca RDS Proxy delante de RDS, y Supabase coloca su propio pooler, Supavisor, delante de Supabase Postgres. Ambos cumplen la función descrita aquí: mantener las conexiones de los clientes con un coste bajo y asignar un número menor de backends reales. Supavisor es de código abierto y se puede autohospedar, por lo que la elección no se reduce a una opción propietaria frente a una gratuita.
El equivalente autohospedado de un proxy gestionado no es una idea distinta. Es la misma idea, con el archivo de configuración bajo su control: PgBouncer en modo de transacción, en el mismo VPS que la base de datos, escuchando en 127.0.0.1. Hay dos diferencias reales. Un proxy gestionado está a un salto de red, por lo que añade latencia y sigue manteniendo las conexiones de los clientes mientras la base de datos se reinicia. PgBouncer en el host de la base de datos añade un salto por loopback, cuyo coste es prácticamente nulo, y deja de funcionar cuando el host falla. Si quiere conservar el comportamiento de supervivencia ante un reinicio, también necesita mecanismos de conmutación por error. Ahí es donde el watchdog de Pgpool-II o las comprobaciones de estado de PgDog empiezan a justificar su complejidad.
Hay otra opción que conviene incluir en la lista. Si el número de conexiones es lo que complica principalmente la implementación, una base de datos integrada no tiene un modelo de conexiones que deba agruparse, porque es una biblioteca dentro de su proceso y no un servidor que escucha en un puerto. En un único servidor de aplicaciones con un volumen de escritura moderado, ejecutar SQLite en producción en un VPS elimina todo este problema en lugar de administrarlo. Cuando necesite un servidor real, dimensione el pool antes que la máquina.
FAQ
¿Sigo necesitando PgBouncer si mi aplicación ya tiene un pool de conexiones?
Normalmente sí, porque el pool de la aplicación es independiente para cada proceso y no puede ver los demás. Cuatro workers de Gunicorn, cada uno con un pool de 20 backends de 80 solicitudes, y dos workers en segundo plano adicionales suman 120. PgBouncer es el único componente que ve el total y puede limitarlo. La configuración adecuada combina ambos: un pool pequeño dentro de cada worker, para que las solicitudes no tengan que abrir una conexión TCP, y PgBouncer en modo de transacción, que limita los backends reales que hay detrás.
¿Qué se rompe exactamente cuando cambio PgBouncer al modo de transacción?
Todo lo que mantiene el estado en un backend entre transacciones. SET y RESET a nivel de sesión, los cursores LISTEN, WITH HOLD, las sentencias SQL PREPARE y DEALLOCATE, los bloqueos consultivos a nivel de sesión, las tablas temporales que deben sobrevivir a un commit y LOAD. NOTIFY sigue funcionando, por lo que un LISTEN roto parece un error de entrega y no un problema del pool. En Django, establezca DISABLE_SERVER_SIDE_CURSORS en True. En psycopg 3, establezca prepare_threshold en None o ejecute PgBouncer 1.22 o posterior con max_prepared_statements por encima de 0. Sustituya pg_advisory_lock() por pg_advisory_xact_lock().
¿Qué tamaño debe tener default_pool_size en una VPS de 2 cores?
Debe ser menor de lo que parece razonable. La heurística de HikariCP publicada habitualmente es aproximadamente el doble del número de cores más uno, es decir, unos 5 en 2 cores. Esto sirve como punto de partida, no como respuesta definitiva. Establézcalo y después lea maxwait y cl_waiting en SHOW POOLS con carga real. Si ambos valores son 0, el pool tiene un tamaño suficiente. Un maxwait creciente indica que los clientes están esperando en cola. Antes de aumentar el valor, compruebe pg_stat_activity: los backends bloqueados en idle in transaction indican un error de la aplicación que más conexiones sólo ocultarán.
¿PgBouncer o PgDog?
PgBouncer para un servidor PostgreSQL en una sola VPS, que es la configuración más habitual. Está empaquetado en Ubuntu, su comportamiento está bien documentado y toda su configuración se encuentra en un único archivo ini. Use PgDog cuando la división de lecturas y escrituras entre réplicas o el sharding deba realizarse en el mismo salto que el pooling, para que la aplicación no tenga que conocer la topología. Antes de decidirse por PgDog, resuelva la cuestión de AGPLv3 con la persona responsable de las licencias en su organización y fije una release específica, porque el proyecto todavía utiliza números de versión 0.x y publica una release cada semana.