{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium","title":"PostgreSQL CDC con Debezium: Replicación Lógica, Decodificación WAL y Arquitectura Event-Driven Real","description":"Una guía técnica profunda de Change Data Capture en PostgreSQL con Debezium, replication slots lógicos, internals de decodificación WAL, el patrón transactional outbox, y construyendo arquitectura event-driven en producción con Kafka Connect. Con configs reales y gotchas de producción.","language":"es","datePublished":"2026-01-13","dateModified":null,"contentFormat":"text/markdown","content":"Databases\n\n# PostgreSQL CDC con Debezium: Replicación Lógica, Decodificación WAL y Arquitectura Event-Driven Real\n\n2026-01-133 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/postgres-logical-replication-cdc-debezium>)\n\n## Funcionamiento de la Decodificación del WAL\n\nEl Write-Ahead Log (WAL) de PostgreSQL registra cada cambio a la base de datos a nivel físico. La replicación lógica agrega una capa de decodificación que traduce estos cambios físicos en operaciones lógicas.\n\n```sql\n-- 1. Habilitar replicación lógica en postgresql.conf\n-- wal_level = logical\n-- max_replication_slots = 10\n\n-- 2. Crear una publicación (qué tablas capturar)\nCREATE PUBLICATION my_publication FOR TABLE\n orders, order_items, customers\n WITH (publish = 'insert, update, delete');\n\n-- 3. Crear un replication slot (cursor del consumidor)\nSELECT pg_create_logical_replication_slot(\n 'debezium_slot', 'pgoutput'\n);\n\n-- Monitorear lag del slot\nSELECT slot_name,\n pg_size_pretty(pg_wal_lsn_diff(\n pg_current_wal_lsn(), confirmed_flush_lsn\n )) AS lag_pretty\nFROM pg_replication_slots\nWHERE slot_type = 'logical';\n```\n\n## El Patrón Transactional Outbox\n\nEste es el patrón que cambia fundamentalmente el enfoque de sistemas event-driven con bases de datos relacionales. En vez de intentar escribir atómicamente tanto a Postgres como a Kafka, escribirs el evento a una tabla outbox dentro de la misma transacción:\n\n```sql\nCREATE TABLE outbox (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n aggregate_type VARCHAR(255) NOT NULL,\n aggregate_id VARCHAR(255) NOT NULL,\n event_type VARCHAR(255) NOT NULL,\n payload JSONB NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\nBEGIN;\n UPDATE orders SET status = 'shipped', shipped_at = now()\n WHERE id = 9001;\n\n INSERT INTO outbox (aggregate_type, aggregate_id, event_type, payload)\n VALUES (\n 'Order', '9001', 'OrderShipped',\n '{\"order_id\": 9001, \"shipped_at\": \"2026-02-28T10:30:00Z\",\n \"tracking_number\": \"1Z999AA10123456784\"}'::jsonb\n );\nCOMMIT;\n-- Ambos tienen éxito o ambos fallan. Sin problema de dual-write.\n```\n\nDebezium captura el insert al outbox y lo rutea al topic de Kafka apropiado. La tabla outbox se puede truncar periódicamente porque Debezium ya capturó los eventos.\n\n## Gotchas de Producción\n\n### Columnas TOAST y eventos UPDATE\n\nPostgreSQL almacena valores de columnas grandes en tablas TOAST. Cuando hacés UPDATE de una fila pero no se modifica una columna TOASTed, el registro WAL no incluye el valor TOAST sin cambios. Debezium ve ` null `.\n\n```sql\n-- Solución: REPLICA IDENTITY FULL\nALTER TABLE orders REPLICA IDENTITY FULL;\n```\n\n### Cambios de schema rompen todo\n\nAgregar una columna a una tabla capturada puede causar errores de mismatch de schema. Siempre agregar con un default primero.\n\n### Bloat del slot durante mantenimiento\n\nDurante pg\\_dump o vacuum full, el LSN no avanza. Si tu backup tarda 4 horas y teners 10K escrituras/segundo, son ~144M registros WAL retenidos.\n\n## La Arquitectura Final\n\n```\n┌─────────────────┐ ┌──────────────┐ ┌───────────────┐\n│ PostgreSQL │ │ Debezium │ │ Kafka │\n│  │ WAL │ (Kafka │ │ │\n│ orders ├────→│ Connect) ├────→│ domain.events │\n│ outbox │ │ │ │ .Order │\n│ order_items │ │ Transforms: │ │ .Customer │\n│ │ │ - Outbox │ │ .OrderItem │\n└─────────────────┘ │ - Route │ └───────┬───────┘\n  └──────────────┘ │\n   ┌──────┴──────┐\n   │ Consumers │\n   │ - Search │\n   │ - Analytics │\n   │ - Notify │\n   └─────────────┘\n```\n\nEl patrón outbox maneja eventos de dominio. CDC directo maneja actualizaciones de vistas materializadas para índices de búsqueda y analytics. Los dos patrones se complementan, outbox para eventos intencionales, CDC para sincronización de datos.\n\nLatencia end-to-end total desde commit en Postgres hasta consumer en Kafka: p50 = 45ms, p99 = 180ms. Para un sistema event-driven construido encima de una base de datos monolítica, ese es números son más que aceptables.\n\n## Preguntas frecuentes\n\n### ¿Qué es Change Data Capture (CDC) y por qué usarlo con PostgreSQL?\n\nCDC captura cambios a nivel de fila (inserts, updates, deletes) de una base de datos y los transmite como eventos. Con PostgreSQL, Debezium usa replicación lógica para leer el Write-Ahead Log (WAL), lo que significa cero impacto en queries de la aplicación y sin overhead de polling. Habilita arquitecturas event-driven en tiempo real sin modificar código de la aplicación.\n\n### ¿Qué son los replication slots lógicos de PostgreSQL?\n\nLos replication slots son cursores del lado del servidor que trackean la posición de un consumidor en el WAL. Garantizan que PostgreSQL retiene segmentos WAL hasta que el consumidor confirma el procesamiento, previniendo pérdida de datos. La desventaja es que un consumidor detenido puede causar acumulación de WAL y agotamiento de disco.\n\n### ¿Cuándo debería usar el patrón transactional outbox?\n\nUtilizar el outbox cuando se necesita actualizar tu base de datos y publicar un evento de forma atómica. En vez de dual-write a Postgres y Kafka, escribirs el evento a una tabla outbox dentro de la misma transacción, y Debezium lo captura vía CDC. Esto garantiza semántica exactly-once entre tu base de datos y message broker.\n\n## Tips\n\n1. **Tip:** Siempre colocar ` slot.drop.on.stop=false ` en conectores Debezium de producción. Si accidentalmente se elimina el replication slot, Postgres va a reciclar los segmentos WAL, y se va a necesitar un re-snapshot completo de todas las tablas. Esta es una lección que comúnmente se aprende de la peor manera durante incidentes nocturnos.\n2. **Tip:** Monitorear ` pg_replication_slots.confirmed_flush_lsn ` vs ` pg_current_wal_lsn() `. Si la brecha sigue creciendo, tu consumer está quedándose atrás y Postgres está reteniendo segmentos WAL, eventualmente se va a quedar sin disco. Configurar una alerta cuando el lag supere 1 GB.\n3. **Tip:** El patrón transactional outbox con Debezium es la única forma confiable de publicar eventos de dominio desde una transacción PostgreSQL. Dual-writes a Postgres y Kafka van a tener *siempre* una ventana de falla donde uno tiene éxito y el otro no. El outbox elimina esto completamente.\n4. **Tip:** Utilizar ` publication.autocreate.mode=filtered ` y listar las tablas explícitamente. El default ` all_tables ` va a capturar todas las tablas incluyendo tablas temporales, logs de auditoría, y esa tabla de migración olvidada, inundando tus topics de Kafka con ruido.\n\n- postgresql\n- cdc\n- debezium\n- kafka\n- replicación-lógica\n- wal\n- event-driven\n- outbox-pattern\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"PostgreSQL CDC con Debezium: Replicación Lógica, Decodificación WAL y Arquitectura Event-Driven Real","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#title"},{"level":2,"text":"Funcionamiento de la Decodificación del WAL","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#funcionamiento-de-la-decodificacin-del-wal"},{"level":2,"text":"El Patrón Transactional Outbox","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#el-patrn-transactional-outbox"},{"level":2,"text":"Gotchas de Producción","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#gotchas-de-produccin"},{"level":3,"text":"Columnas TOAST y eventos UPDATE","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#columnas-toast-y-eventos-update"},{"level":3,"text":"Cambios de schema rompen todo","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#cambios-de-schema-rompen-todo"},{"level":3,"text":"Bloat del slot durante mantenimiento","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#bloat-del-slot-durante-mantenimiento"},{"level":2,"text":"La Arquitectura Final","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#la-arquitectura-final"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#questions"},{"level":3,"text":"¿Qué es Change Data Capture (CDC) y por qué usarlo con PostgreSQL?","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#question-1"},{"level":3,"text":"¿Qué son los replication slots lógicos de PostgreSQL?","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#question-2"},{"level":3,"text":"¿Cuándo debería usar el patrón transactional outbox?","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#article-tips"}],"sections":[{"heading":"PostgreSQL CDC con Debezium: Replicación Lógica, Decodificación WAL y Arquitectura Event-Driven Real","level":1,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium","content":"Databases"},{"heading":"PostgreSQL CDC con Debezium: Replicación Lógica, Decodificación WAL y Arquitectura Event-Driven Real","level":1,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#title","content":"2026-01-133 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/postgres-logical-replication-cdc-debezium>)"},{"heading":"Funcionamiento de la Decodificación del WAL","level":2,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#funcionamiento-de-la-decodificacin-del-wal","content":"El Write-Ahead Log (WAL) de PostgreSQL registra cada cambio a la base de datos a nivel físico. La replicación lógica agrega una capa de decodificación que traduce estos cambios físicos en operaciones lógicas.\n\n```sql\n-- 1. Habilitar replicación lógica en postgresql.conf\n-- wal_level = logical\n-- max_replication_slots = 10\n\n-- 2. Crear una publicación (qué tablas capturar)\nCREATE PUBLICATION my_publication FOR TABLE\n orders, order_items, customers\n WITH (publish = 'insert, update, delete');\n\n-- 3. Crear un replication slot (cursor del consumidor)\nSELECT pg_create_logical_replication_slot(\n 'debezium_slot', 'pgoutput'\n);\n\n-- Monitorear lag del slot\nSELECT slot_name,\n pg_size_pretty(pg_wal_lsn_diff(\n pg_current_wal_lsn(), confirmed_flush_lsn\n )) AS lag_pretty\nFROM pg_replication_slots\nWHERE slot_type = 'logical';\n```"},{"heading":"El Patrón Transactional Outbox","level":2,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#el-patrn-transactional-outbox","content":"Este es el patrón que cambia fundamentalmente el enfoque de sistemas event-driven con bases de datos relacionales. En vez de intentar escribir atómicamente tanto a Postgres como a Kafka, escribirs el evento a una tabla outbox dentro de la misma transacción:\n\n```sql\nCREATE TABLE outbox (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n aggregate_type VARCHAR(255) NOT NULL,\n aggregate_id VARCHAR(255) NOT NULL,\n event_type VARCHAR(255) NOT NULL,\n payload JSONB NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT now()\n);\n\nBEGIN;\n UPDATE orders SET status = 'shipped', shipped_at = now()\n WHERE id = 9001;\n\n INSERT INTO outbox (aggregate_type, aggregate_id, event_type, payload)\n VALUES (\n 'Order', '9001', 'OrderShipped',\n '{\"order_id\": 9001, \"shipped_at\": \"2026-02-28T10:30:00Z\",\n \"tracking_number\": \"1Z999AA10123456784\"}'::jsonb\n );\nCOMMIT;\n-- Ambos tienen éxito o ambos fallan. Sin problema de dual-write.\n```\n\nDebezium captura el insert al outbox y lo rutea al topic de Kafka apropiado. La tabla outbox se puede truncar periódicamente porque Debezium ya capturó los eventos."},{"heading":"Gotchas de Producción","level":2,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#gotchas-de-produccin","content":""},{"heading":"Columnas TOAST y eventos UPDATE","level":3,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#columnas-toast-y-eventos-update","content":"PostgreSQL almacena valores de columnas grandes en tablas TOAST. Cuando hacés UPDATE de una fila pero no se modifica una columna TOASTed, el registro WAL no incluye el valor TOAST sin cambios. Debezium ve ` null `.\n\n```sql\n-- Solución: REPLICA IDENTITY FULL\nALTER TABLE orders REPLICA IDENTITY FULL;\n```"},{"heading":"Cambios de schema rompen todo","level":3,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#cambios-de-schema-rompen-todo","content":"Agregar una columna a una tabla capturada puede causar errores de mismatch de schema. Siempre agregar con un default primero."},{"heading":"Bloat del slot durante mantenimiento","level":3,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#bloat-del-slot-durante-mantenimiento","content":"Durante pg\\_dump o vacuum full, el LSN no avanza. Si tu backup tarda 4 horas y teners 10K escrituras/segundo, son ~144M registros WAL retenidos."},{"heading":"La Arquitectura Final","level":2,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#la-arquitectura-final","content":"```\n┌─────────────────┐ ┌──────────────┐ ┌───────────────┐\n│ PostgreSQL │ │ Debezium │ │ Kafka │\n│  │ WAL │ (Kafka │ │ │\n│ orders ├────→│ Connect) ├────→│ domain.events │\n│ outbox │ │ │ │ .Order │\n│ order_items │ │ Transforms: │ │ .Customer │\n│ │ │ - Outbox │ │ .OrderItem │\n└─────────────────┘ │ - Route │ └───────┬───────┘\n  └──────────────┘ │\n   ┌──────┴──────┐\n   │ Consumers │\n   │ - Search │\n   │ - Analytics │\n   │ - Notify │\n   └─────────────┘\n```\n\nEl patrón outbox maneja eventos de dominio. CDC directo maneja actualizaciones de vistas materializadas para índices de búsqueda y analytics. Los dos patrones se complementan, outbox para eventos intencionales, CDC para sincronización de datos.\n\nLatencia end-to-end total desde commit en Postgres hasta consumer en Kafka: p50 = 45ms, p99 = 180ms. Para un sistema event-driven construido encima de una base de datos monolítica, ese es números son más que aceptables."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#questions","content":""},{"heading":"¿Qué es Change Data Capture (CDC) y por qué usarlo con PostgreSQL?","level":3,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#question-1","content":"CDC captura cambios a nivel de fila (inserts, updates, deletes) de una base de datos y los transmite como eventos. Con PostgreSQL, Debezium usa replicación lógica para leer el Write-Ahead Log (WAL), lo que significa cero impacto en queries de la aplicación y sin overhead de polling. Habilita arquitecturas event-driven en tiempo real sin modificar código de la aplicación."},{"heading":"¿Qué son los replication slots lógicos de PostgreSQL?","level":3,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#question-2","content":"Los replication slots son cursores del lado del servidor que trackean la posición de un consumidor en el WAL. Garantizan que PostgreSQL retiene segmentos WAL hasta que el consumidor confirma el procesamiento, previniendo pérdida de datos. La desventaja es que un consumidor detenido puede causar acumulación de WAL y agotamiento de disco."},{"heading":"¿Cuándo debería usar el patrón transactional outbox?","level":3,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#question-3","content":"Utilizar el outbox cuando se necesita actualizar tu base de datos y publicar un evento de forma atómica. En vez de dual-write a Postgres y Kafka, escribirs el evento a una tabla outbox dentro de la misma transacción, y Debezium lo captura vía CDC. Esto garantiza semántica exactly-once entre tu base de datos y message broker."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#article-tips","content":"1. **Tip:** Siempre colocar ` slot.drop.on.stop=false ` en conectores Debezium de producción. Si accidentalmente se elimina el replication slot, Postgres va a reciclar los segmentos WAL, y se va a necesitar un re-snapshot completo de todas las tablas. Esta es una lección que comúnmente se aprende de la peor manera durante incidentes nocturnos.\n2. **Tip:** Monitorear ` pg_replication_slots.confirmed_flush_lsn ` vs ` pg_current_wal_lsn() `. Si la brecha sigue creciendo, tu consumer está quedándose atrás y Postgres está reteniendo segmentos WAL, eventualmente se va a quedar sin disco. Configurar una alerta cuando el lag supere 1 GB.\n3. **Tip:** El patrón transactional outbox con Debezium es la única forma confiable de publicar eventos de dominio desde una transacción PostgreSQL. Dual-writes a Postgres y Kafka van a tener *siempre* una ventana de falla donde uno tiene éxito y el otro no. El outbox elimina esto completamente.\n4. **Tip:** Utilizar ` publication.autocreate.mode=filtered ` y listar las tablas explícitamente. El default ` all_tables ` va a capturar todas las tablas incluyendo tablas temporales, logs de auditoría, y esa tabla de migración olvidada, inundando tus topics de Kafka con ruido.\n\n- postgresql\n- cdc\n- debezium\n- kafka\n- replicación-lógica\n- wal\n- event-driven\n- outbox-pattern\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/postgres-logical-replication-cdc-debezium"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium"},"headline":"PostgreSQL CDC con Debezium: Replicación Lógica, Decodificación WAL y Arquitectura Event-Driven Real","description":"Una guía técnica profunda de Change Data Capture en PostgreSQL con Debezium, replication slots lógicos, internals de decodificación WAL, el patrón transactional outbox, y construyendo arquitectura event-driven en producción con Kafka Connect. Con configs reales y gotchas de producción.","datePublished":"2026-01-13","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Databases","keywords":"postgresql, cdc, debezium, kafka, replicación-lógica, wal, event-driven, outbox-pattern","wordCount":519,"timeRequired":"PT3M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/postgres-logical-replication-cdc-debezium"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"PostgreSQL CDC con Debezium: Replicación Lógica, Decodificación WAL y Arquitectura Event-Driven Real","item":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Qué es Change Data Capture (CDC) y por qué usarlo con PostgreSQL?","acceptedAnswer":{"@type":"Answer","text":"CDC captura cambios a nivel de fila (inserts, updates, deletes) de una base de datos y los transmite como eventos. Con PostgreSQL, Debezium usa replicación lógica para leer el Write-Ahead Log (WAL), lo que significa cero impacto en queries de la aplicación y sin overhead de polling. Habilita arquitecturas event-driven en tiempo real sin modificar código de la aplicación."}},{"@type":"Question","name":"¿Qué son los replication slots lógicos de PostgreSQL?","acceptedAnswer":{"@type":"Answer","text":"Los replication slots son cursores del lado del servidor que trackean la posición de un consumidor en el WAL. Garantizan que PostgreSQL retiene segmentos WAL hasta que el consumidor confirma el procesamiento, previniendo pérdida de datos. La desventaja es que un consumidor detenido puede causar acumulación de WAL y agotamiento de disco."}},{"@type":"Question","name":"¿Cuándo debería usar el patrón transactional outbox?","acceptedAnswer":{"@type":"Answer","text":"Utilizar el outbox cuando se necesita actualizar tu base de datos y publicar un evento de forma atómica. En vez de dual-write a Postgres y Kafka, escribirs el evento a una tabla outbox dentro de la misma transacción, y Debezium lo captura vía CDC. Esto garantiza semántica exactly-once entre tu base de datos y message broker."}}]}]}],"contentHash":"9ee9338a39b64c3d618108da28706c627e4662dc6c24928ec5c58f91b6bb2acc","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/postgres-logical-replication-cdc-debezium","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":49626,"contentBytes":7047}}
