LISTEN/NOTIFY de Postgres sí escala: DBOS logra 60.000 escrituras por segundo con streaming de baja latencia

🕒 Publicado en Zendoric: 28 de julio de 2026 · 00:38
DBOS ha publicado un artículo técnico que intenta rehabilitar la reputación de LISTEN/NOTIFY, el mecanismo nativo de pub/sub de PostgreSQL, al que un post previo muy citado acusó de "no escalar".
DBOS ha publicado un artículo técnico que intenta rehabilitar la reputación de LISTEN/NOTIFY, el mecanismo nativo de pub/sub de PostgreSQL, al que un post previo muy citado acusó de "no escalar". La entrada, firmada por Peter Kraft y publicada el 24 de julio, no niega el problema de fondo, sino que lo explica con detalle y muestra cómo evitarlo: con las optimizaciones adecuadas, su implementación de streams basada en LISTEN/NOTIFY llega a 60.000 escrituras por segundo en un único servidor Postgres, con latencias de milisegundos.
El caso de uso que motiva el análisis es el streaming de baja latencia: por ejemplo, ir entregando token a token la respuesta de un modelo de lenguaje. El diseño básico es sencillo —una tabla de "streams" donde cada fragmento nuevo es una fila insertada—, pero leer ese flujo es el problema difícil, porque el lector no sabe cuándo llegará el siguiente fragmento. La alternativa obvia, el polling, obliga a elegir entre dos males: un intervalo de sondeo alto introduce demasiada latencia para casos interactivos como un chat en vivo, y uno bajo satura la base de datos con lectores concurrentes. LISTEN/NOTIFY resuelve esto en teoría, porque permite que los lectores se queden bloqueados esperando una notificación en lugar de sondear activamente, despertando de inmediato cuando llega un dato nuevo.
La primera implementación de DBOS usaba un trigger en la tabla de streams que disparaba una notificación cada vez que se escribía un fragmento nuevo. Funcionaba correctamente y con baja latencia, pero su throughput era pobre: incluso con una base de datos grande, no lograba sostener más de 2.900 escrituras de stream por segundo. Lo llamativo es que este cuello de botella aparecía sin que se observara un consumo visible de recursos de Postgres —ni CPU, ni memoria, ni IOPS—, señal de que el problema no era de capacidad sino de contención.
El artículo dedica su parte más sustanciosa a explicar el origen exacto de esa contención: confirmar una transacción que llama a NOTIFY exige tomar un bloqueo exclusivo global. Ese bloqueo se adquiere al iniciar el commit y no se libera hasta que la transacción está completamente confirmada, incluyendo el volcado a disco vía fsync(). La razón de este diseño es que Postgres garantiza que las notificaciones se entregan en el mismo orden en que se confirmaron las transacciones que las generaron: internamente mantiene una cola global de notificaciones salientes cuyo orden debe coincidir exactamente con el orden de commit, y añadir elementos a esa cola debe hacerse de forma transaccional como parte del propio commit. El problema es que Postgres no asigna un orden de commit a una transacción hasta que esta termina de confirmarse —y ese proceso puede tardar un tiempo variable—, lo que genera una paradoja de ordenación: hace falta saber el orden de commit para insertar en la cola, pero ese orden no existe hasta que el commit concluye. La solución de Postgres es, precisamente, el bloqueo global: serializa los commits de todas las transacciones que incluyen notificaciones, de modo que su orden queda fijado de antemano.
Como el trigger original disparaba NOTIFY en cada escritura de stream, cada una de esas escrituras debía tomar el bloqueo global y mantenerlo durante todo el commit, disco incluido. Esto forzaba a que las escrituras se confirmaran de forma estrictamente secuencial, impidiendo que Postgres aplicara optimizaciones habituales como el group commit (agrupar muchas transacciones en un único fsync()). El throughput quedaba entonces limitado por la velocidad de confirmación de transacciones una por una, lo que explica tanto el techo de 2.900 escrituras por segundo como la ausencia de consumo visible de CPU o disco: todo estaba serializado por un lock, no por falta de capacidad de la máquina.
El texto añade un apunte relevante sobre un parche de Postgres, previsto para la versión 19, que ha generado cierta discusión pública sobre este mismo tema. Según DBOS, ese parche no elimina el bloqueo global ni resuelve el cuello de botella descrito: se limita a optimizar un caso más acotado, aquel en que existen muchos canales de notificación distintos y cada listener espera solo en un canal específico. Es una precisión útil para no sobreestimar el alcance de los cambios que llegarán en próximas versiones del motor.
La solución que propone DBOS parte de una observación conceptual: en los streams —y en muchos otros usos de LISTEN/NOTIFY— la notificación en sí no es la fuente de verdad, sino simplemente un aviso para que el lector revise la tabla de la base de datos, que es donde reside el dato real. Como consecuencia, las notificaciones no necesitan estar perfectamente ordenadas ni ser absolutamente duraderas, lo que abre la puerta a optimizarlas: en vez de emitir un NOTIFY por cada escritura, se acumulan en un búfer en memoria y se vuelcan periódicamente en una única transacción por lotes. Esto reduce drásticamente la contención sobre el bloqueo global, porque este solo se toma en el momento de vaciar el búfer y no en cada escritura individual. Las escrituras de stream individuales pueden entonces confirmarse rápido, aprovechando de nuevo el group commit, mientras el vaciado del búfer ocurre en segundo plano.
Esta estrategia introduce, eso sí, una nueva complicación: si el proceso se cae mientras hay notificaciones acumuladas en el búfer sin volcar, esas notificaciones se pierden para siempre. DBOS lo resuelve con un mecanismo de respaldo del lado del lector: además de esperar notificaciones, los lectores sondean periódicamente la base de datos para comprobar si el stream recibió escrituras que no llegaron a notificarse. Como este sondeo es solo una red de seguridad para casos excepcionales, su frecuencia puede ser baja y apenas afecta al rendimiento general.
Con esta versión optimizada, las cifras mejoran de forma sustancial: hasta 60.000 escrituras de stream por segundo con lectores concurrentes activos —veinte veces más que la implementación inicial— manteniendo latencias de entre 15 y 100 milisegundos. En el punto de máximo throughput, la CPU de Postgres queda totalmente utilizada, lo que para DBOS es la prueba de que ahora el sistema está genuinamente saturado por trabajo real y no bloqueado por contención artificial de un lock.
Más allá del detalle técnico, el artículo deja una lección de fondo aplicable a cualquiera que construya infraestructura de pub/sub o streaming sobre Postgres: un comportamiento "no intuitivo" y mal documentado —como el bloqueo global de NOTIFY— no equivale automáticamente a "no escala", pero sí exige entender el mecanismo interno antes de descartar la herramienta o de lanzarse a arquitecturas con colas y brokers externos (Kafka, Redis, etc.) cuando quizá baste con rediseñar cómo se emiten las notificaciones. DBOS publica el código del benchmark en GitHub (dbos-inc/dbos-postgres-benchmark) para quien quiera reproducir los números, algo que aporta transparencia a las cifras reportadas, aunque conviene recordar que provienen del propio proveedor y no de una evaluación independiente.
🔗 Relacionadas en Zendoric
- The Economist: la fabricación de chips se vuelve vertical al agotarse la miniaturización tipo Silicon Valley · 2026-07-10
- El sesgo etario de ChatGPT-4o no es un fallo técnico: es la sociedad reflejada a escala industrial (KAIST lo cuantifica) · 2026-06-28
- Apple busca permiso para comprar chips de memoria a una empresa china en lista negra · 2026-06-30


