Registro / artículo
Kafka no es una cola de mensajes: qué resuelve y cuándo no usarlo
A las 23:47 entra un pedido. Tu API cobra la tarjeta, marca la orden como pagada, llama al servicio de cocina, espera la respuesta, llama al de envíos, espera, y por último llama a facturación. Todo dentro de la misma petición HTTP, porque así es más simple.
Funciona. Hasta que no funciona.
Ese diseño tiene tres formas de romperse y las tres aparecen el mismo viernes:
- Latencia acumulada. Cada servicio suma su tiempo. Si cocina tarda 300 ms, envíos 200 ms y facturación 400 ms, el cliente espera casi un segundo de trabajo ajeno antes de ver su confirmación.
- Disponibilidad multiplicada. Si cada servicio tiene 99,9% de disponibilidad, la cadena completa está en 99,6%. Con cuatro servicios, has multiplicado por cuatro el tiempo de caída sin escribir una sola línea mal.
- Consistencia partida. Facturación se cae. El pedido quedó pagado, la cocina no se enteró y el envío no existe. El usuario reintenta y ahora tienes dos cobros y un pedido fantasma.
El problema no es que falte una herramienta. Es que el pedido está atado en el tiempo a cuatro servicios que no necesitan conocerse.
La solución obvia es hacerlo asíncrono: encolar el evento y que cada servicio lo procese a su ritmo. Correcto. Pero “encolar” no es una sola cosa, y ahí empieza el problema real.
Una cola y un log no son lo mismo
Una cola tradicional —SQS, RabbitMQ— es un buzón. El cartero se lleva la carta y la carta desaparece. Si el consumidor confirma, el mensaje se borra; si no confirma, vuelve a la cola. Es un mecanismo de reparto de trabajo: varios consumidores compiten por el mismo mensaje y cada mensaje se entrega a un solo consumidor.
Kafka es un libro de actas. Los eventos se escriben al final y no se borran cuando alguien los lee. Cada lector lleva su propio marcapáginas. Diez equipos pueden leer el mismo tema sin coordinarse, y uno puede volver atrás tres días si descubre un bug.
Piensa en una panadería. Cada pedido se anota en un cuaderno, por orden, y no se tacha cuando se atiende. El equipo del horno, el de la caja y el del reparto leen el mismo cuaderno, pero cada equipo con su propia pinza marcando la última página que ha procesado. Si el equipo del horno se para una hora, al volver sigue desde su pinza y se pone al día sin que nadie le repita los pedidos. Y si alguien descubre que un pedido se anotó mal, puede volver a leer esa página.
Esa diferencia parece académica. No lo es: cambia qué puedes hacer cuando algo sale mal.
| Cola (SQS, RabbitMQ) | Log (Kafka) | |
|---|---|---|
| Al leer | el mensaje se confirma y desaparece | queda escrito |
| Varios consumidores | compiten por el mismo mensaje | cada grupo avanza su offset |
| Reprocesar | complicado, hay que reencolar | retroceder el offset |
| Orden | limitado, por cola | por partición |
| Retención | hasta consumir o TTL | por tiempo o tamaño |
| Uso típico | trabajos, tareas | flujos de eventos, auditoría |
Si lo que necesitas es repartir tareas con reintentos, prioridades y retardos por mensaje, Kafka no es la herramienta. Volvemos a eso al final.
Qué es un topic, en serio
Un topic es un nombre con un log detrás. Ese log está partido en particiones, y cada partición es un archivo que solo crece por el final:
orders.created
├── partition 0: [0] [1] [2] [3] [4] [5] ...
├── partition 1: [0] [1] [2] [3] ...
└── partition 2: [0] [1] [2] [3] [4] ...
Cada registro tiene clave, valor, timestamp y headers. El offset es su posición dentro de la partición: un número que solo sube.
Un productor escribe al final y elige partición. Un consumidor lee desde un offset y avanza. El broker guarda esos archivos en disco y los replica en otros brokers.
Hay dos consecuencias que conviene tener claras desde el minuto uno:
- No hay update ni delete. Un topic no es una tabla. Si un pedido cambia de estado, escribes otro evento. El estado actual se reconstruye leyendo la secuencia.
- No hay búsqueda por clave. Para leer, avanzas. Si necesitas consultar “el pedido 1234”, eso es una base de datos, no Kafka.
Kafka no es una base de datos, pero tampoco es una cola. Es un registro de eventos con retención. La mayoría de los malentendidos vienen de tratarlo como cualquiera de las otras dos cosas.
Offsets y grupos: el detalle que define tu arquitectura
Un consumer group es un equipo de lectores que se reparten las particiones. Cada grupo tiene su propio offset confirmado, independiente del resto.
Eso significa que puedes tener, sobre el mismo topic:
- el grupo
kitchen, que prepara pedidos; - el grupo
billing, que factura; - el grupo
analytics, que calcula métricas; - el grupo
audit, que archiva todo en un data lake.
Los cuatro leen lo mismo. Ninguno le quita eventos a los otros. Leer no borra.
Dentro de un grupo, la unidad de paralelismo es la partición. Si tienes 6 particiones, el grupo puede tener como máximo 6 consumidores útiles. El séptimo se queda mirando. Es la razón número uno por la que la gente descubre, en producción, que su paralelismo no escala: añadieron consumers, no particiones.
El offset que se confirma es el siguiente mensaje a leer, no el último procesado. Y ahí está la decisión más importante de todo el sistema:
- Confirmas después de procesar: si el proceso muere en el medio, el mensaje se vuelve a entregar. Es at-least-once.
- Confirmas antes de procesar: si el proceso muere, el mensaje se perdió. Es at-most-once.
Kafka, en la práctica, te da la primera si confirmas después de procesar, que es lo que deberías hacer. Y eso significa una cosa incómoda: vas a procesar duplicados.
La duplicación no es un bug, es el contrato
Cada vez que alguien dice “Kafka me duplicó el cobro”, en realidad está diciendo “mi consumidor no es idempotente”. El broker cumplió: entregó al menos una vez. Tu código asumió exactamente una.
La solución no es configurar una opción mágica. La solución es que procesar dos veces el mismo evento tenga el mismo efecto que procesarlo una:
al recibir OrderCreated(event_id, order_id, total):
si event_id ya está en processed_events:
confirmar offset
return
transacción:
insertar en processed_events (event_id)
cobrar(order_id, idempotency_key = order_id)
marcar orden como pagada
confirmar transacción
confirmar offset
Tres detalles que hacen que esto funcione de verdad:
- La clave de idempotencia la genera el productor, no el consumidor.
event_idviaja en el evento y se repite en el reintento. - El registro de procesados y el efecto van en la misma transacción. Si los separas, volviste al problema original.
- La unicidad la garantiza la base de datos, con un índice único. Un
if existsen memoria no sobrevive a un reinicio ni a dos instancias en paralelo.
¿Y el famoso exactly-once? Kafka lo ofrece con productor idempotente y transacciones, pero el alcance es concreto: funciona cuando lees de Kafka y escribes en Kafka. En el momento en que tu consumidor cobra una tarjeta o hace un INSERT en Postgres, sales de la transacción de Kafka y vuelves al mundo real, donde la idempotencia la pones tú.
“Exactly-once” es un buen titular y una pésima promesa si no aclaras dónde termina.
Particiones: la clave decide tu orden
Kafka no garantiza orden global. Garantiza orden dentro de una partición. Si dos eventos del mismo pedido caen en particiones distintas, pueden procesarse al revés y tu máquina de estados se rompe.
La clave del mensaje resuelve eso:
partition = hash(key) % número_de_particiones
Si usas order_id como clave, todos los eventos de ese pedido van a la misma partición y llegan en orden. Si no pones clave, el productor reparte entre particiones sin garantía de orden.
El otro lado de la moneda es la partición caliente. Si usas customer_id y un cliente genera el 40% del tráfico, el 40% de los eventos cae en una sola partición y tu paralelismo se evapora. La clave no se elige por comodidad: se elige por la unidad que necesita orden.
Y el número de particiones es una decisión bastante definitiva:
- Se puede aumentar, pero no reducir.
- Al aumentar, cambia el mapeo
hash(key) % N: los eventos nuevos de una clave pueden caer en otra partición y romper el orden respecto de los viejos. - Cada partición consume file handles, memoria y ancho de banda de replicación.
Regla práctica: aprovisiona para el pico del próximo año, no para el tráfico de hoy. Un topic con 12 o 24 particiones no es caro; quedarte sin paralelismo un Black Friday sí.
Retención y replay: lo que una cola no te da
Kafka guarda los eventos aunque ya se hayan leído. Por defecto, siete días (retention.ms), y también puedes limitar por tamaño (retention.bytes).
Eso habilita la operación más valiosa del sistema: reprocesar. Descubriste que el cálculo de impuestos estaba mal desde el martes. Corriges el consumidor, retrocedes los offsets y vuelves a pasar tres días de eventos:
kafka-consumer-groups.sh \
--bootstrap-server localhost:9092 \
--group billing \
--topic orders.created \
--reset-offsets --to-datetime 2026-10-19T00:00:00.000 \
--execute
Sin reencolar nada. Sin pedirle a nadie que reenvíe mensajes. El log ya los tenía.
Dos advertencias:
- El replay es seguro solo si el consumidor es idempotente. Si no lo es, acabas de duplicar todos los cobros de tres días. La idempotencia no era una buena práctica: es el permiso para usar replay.
- La retención tiene que cubrir tu peor caída. Si un consumidor puede estar caído un fin de semana, siete días alcanzan. Si el incidente se descubre a la semana y media, no.
Existe además un tipo especial de topic: los compacted. En vez de borrar por tiempo, conservan el último valor de cada clave. Sirven para reconstruir estado —configuración, perfiles, saldos— y para arrancar un servicio nuevo sin leer el historial completo. Un evento con valor null es un tombstone: marca la clave como borrada.
Los números que importan en producción
Kafka tiene fama de difícil. La mayor parte de esa fama viene de configuración, no del concepto. Lo mínimo que necesitas entender:
- Replicación 3,
min.insync.replicas=2,acks=all. Conacks=1el líder confirma antes de que las réplicas copien. Si el líder muere en ese instante, perdiste escrituras que el productor creía seguras. - Desde Kafka 4.0, ZooKeeper ya no existe. KRaft es el único modo de coordinación. Si un tutorial te hace levantar ZooKeeper, es material viejo.
- El cuello de botella suele ser la red, no el disco. Kafka escribe secuencialmente y usa page cache del sistema operativo. Por eso conviene dejarle RAM al SO en lugar de inflar el heap de la JVM.
- El límite por mensaje es 1 MB por defecto. Para archivos o payloads grandes, guarda el binario en S3 y manda un puntero. Es el patrón claim-check, y evita el incidente de “un mensaje gigante bloquea la partición”.
- No hay dead letter queue nativa. El patrón es un topic de reintentos con backoff y, después, un topic de descarte. Escribirlo es parte del trabajo.
- Monitoriza el lag por grupo, no la CPU del broker.
consumer lages la única métrica que te dice si el sistema está sano. Un broker relajado con consumidores atrasados seis horas es un incidente silencioso.
Y una decisión de equipo antes que técnica: ¿administras tú el clúster o pagas uno gestionado? Un clúster mínimo serio son tres brokers, almacenamiento, réplicas y egreso. Autogestionado es más barato en factura y bastante más caro en guardias. MSK, Confluent Cloud o Redpanda te quitan la operación; el precio arranca en cientos de dólares al mes, no en decenas.
Cuándo NO usar Kafka
Esta es la parte que casi ningún tutorial pone primero, y debería.
No uses Kafka si:
- Tu volumen es de miles de eventos al día. Una tabla en Postgres con un worker y
FOR UPDATE SKIP LOCKEDresuelve lo mismo con una fracción de la complejidad. - Necesitas request/response. Kafka no es un bus RPC. Se puede simular con topics de respuesta, y es un anti-patrón que vas a lamentar.
- Necesitas retardos por mensaje (“ejecuta esto en 20 minutos”) o prioridades. Kafka no las tiene: son características de una cola.
- Tu necesidad real es notificar a otro servicio. Un webhook con reintentos y una tabla de outbox alcanzan.
- Tienes un equipo de dos personas sin guardias. Un clúster mal operado es peor que un monolito aburrido.
- Lo que quieres es consultar por ID. Eso es una base de datos. Kafka no reemplaza tu Postgres.
Usa Kafka si:
- Varios equipos necesitan consumir el mismo flujo de eventos de forma independiente.
- El volumen es alto y sostenido, y necesitas paralelismo horizontal por partición.
- Necesitas reprocesar historia: auditoría, corrección de bugs, reconstrucción de estado.
- El orden por entidad —pedido, cuenta, dispositivo— es un requisito del negocio.
- Puedes asumir el coste operativo y tienes idempotencia resuelta.
| Necesidad | Herramienta razonable |
|---|---|
| Jobs con reintentos y backoff | Postgres + SKIP LOCKED, o SQS |
| Fan-out a varios consumidores con replay | Kafka |
| Request/response entre servicios | HTTP o gRPC |
| Prioridades, TTL y retardos por mensaje | RabbitMQ |
| Auditoría y proyección de estado | Kafka (topic compactado) |
| Consultas por ID | PostgreSQL |
El pedido de las 23:47, ahora con Kafka
Volvamos al principio, pero con el modelo correcto.
- La API escribe el pedido y un registro en la tabla
outbox, en la misma transacción. No publica directo a Kafka: si publicara antes del commit, el evento podría existir sin el pedido; si publicara después, el proceso podría morir entre el commit y el publish y el evento se perdería. Es el problema del dual write, y el outbox lo resuelve. - Un relay lee el outbox y publica
order.createdconorder_idcomo clave yacks=all. Marca la fila como enviada. - El grupo
kitchenconsume, prepara el ticket y confirma. El grupobillingconsume, cobra conidempotency_key=order_idy confirma. El gruposhippinghace lo suyo. Nadie espera a nadie. - La respuesta al usuario sale apenas se confirma el pedido: un
INSERT, unINSERTen outbox y un publish. Decenas de milisegundos en vez de un segundo de llamadas en cadena. - Si facturación se cae diez minutos, no pasa nada: se atrasa. Cuando vuelve, retoma desde su offset y se pone al día. Nada se perdió porque nadie le borró los eventos.
- Si dentro de un mes descubres que el cálculo de envío estaba mal, corriges el consumidor, retrocedes el offset del grupo
shippingy reprocesas. Solo funciona porque el handler es idempotente.
Lo que cambió no es la herramienta. Cambió el acoplamiento: los servicios dejaron de estar atados en el tiempo. Ahora están atados al contrato del evento, que es un acoplamiento mucho más barato de evolucionar.
Checklist antes de adoptar Kafka
Si después de leer esto sigues convencido, no arranques sin responder esto:
- ¿Cuál es la entidad que necesita orden? Esa es la clave, no el
user_id. - ¿Cuántas particiones necesito para el pico del próximo año?
- ¿Cuántos días de retención cubren mi peor caída y mi peor bug?
- ¿Cada consumidor es idempotente, con unicidad garantizada por la base de datos?
- ¿Tengo outbox para no perder eventos entre la transacción y el publish?
- ¿Tengo alertas de lag por grupo y un plan para los mensajes que fallan siempre?
- ¿He definido esquemas —Avro, Protobuf— o voy a mandar JSON sin contrato y rezar?
Kafka no te salva de un mal diseño. Te da una segunda oportunidad para arreglarlo, y las segundas oportunidades se pagan con idempotencia. Si tu consumidor no la tiene, el log se convierte en una máquina de repetir errores más rápido.
Si quieres ver cómo encaja esto con el resto de la infraestructura, te recomiendo leer las cinco capas de la caché web y el setup mínimo de DevOps. Kafka resuelve el movimiento de eventos; no resuelve ni la latencia de tus consultas ni la falta de observabilidad.