Webhooks vs Polling vs API: Cómo Elegir un Patrón de Integración (2026) — Cesar Ayala
← Todos los artículos

Webhooks vs Polling vs API: Cómo Elegir un Patrón de Integración (2026)

Llama la API directamente cuando necesitas datos bajo demanda y controlas tú el tiempo. Usa polling cuando la fuente no empuja eventos o no puedes recibir conexiones entrantes. Usa webhooks cuando necesitas reaccionar casi en tiempo real. En producción, combina webhooks para la velocidad con un poll de reconciliación que atrape lo perdido.

API, polling o webhook: ¿cuál deberías usar de verdad?

Llama la API directamente cuando necesitas datos bajo demanda y controlas tú el tiempo. Usa polling cuando la fuente no empuja eventos o no puedes recibir conexiones entrantes. Usa webhooks cuando necesitas reaccionar casi en tiempo real. En producción, combina webhooks para la velocidad con un poll de reconciliación que atrape lo perdido.

Esa es la respuesta corta, pero deja fuera la opinión que de verdad importa, así que la digo de frente: los webhooks son una optimización de latencia, no una fuente de verdad. La fuente de verdad es la API del proveedor; el poll de reconciliación es lo que te mantiene honesto. Si dos sistemas hablan y de por medio hay dinero o acceso —cobrar una factura, otorgar un permiso, provisionar una cuenta— necesitas el patrón híbrido, no elegir un ganador. Porque los webhooks se van a perder: un deploy tumba tu endpoint, un 502 se come el evento, el proveedor se rinde después de su ventana de reintentos. Eso no es un caso raro; es martes.

Te lo digo con cicatrices. Construyendo el cobro de FinHOA con Stripe —el patrón que documenté a fondo en cómo integro Stripe en un SaaS— el único bug que de verdad despierta a las 3 a.m. es la deriva silenciosa de datos: un cliente que pagó, un webhook que nunca llegó, y nadie que lo notara hasta que llamó a soporte. El cron de reconciliación es lo que atrapó esos casos antes de que el cliente lo notara.

Todo el post cuelga de una sola pregunta: ¿quién inicia, y cuándo? Las tres respuestas son los tres patrones. El resto —latencia, fiabilidad, costo, quién necesita un endpoint público— cae solo de ahí.

¿Cuál es la diferencia real entre una llamada API, polling y un webhook?

El modelo mental, en una frase por patrón. El eje que decide todo es la dirección de iniciación (pull vs push) y el timing (bajo demanda vs en agenda vs por evento).

  • API (request/response): TÚ jalas bajo demanda. Síncrono, controlas el tiempo, los datos más frescos posibles —pero solo cuando preguntas. Es un GET en el momento que un usuario pide algo.
  • Polling: TÚ jalas en una agenda. “¿Algo nuevo desde el cursor X?”, en un loop sobre un timer. Tu intervalo es tu piso de latencia. Es literalmente una llamada API dentro de un while.
  • Webhooks: ELLOS empujan. El proveedor te hace un POST HTTP por cada evento discreto a tu URL pública. La latencia más baja, pero invierte el control: ahora tú corres y expones un endpoint público, siempre encendido y seguro, y eres dueño de las fallas de entrega.
  • Streaming (WebSocket/SSE): una conexión persistente para datos continuos de alta frecuencia. Es otro eje —lo cubro más abajo— y es la herramienta equivocada para eventos de negocio discretos como “pago exitoso”.

La pregunta que decide: ¿quién inicia, y sobre la agenda de quién llegan los datos frescos? Si la respuesta es “yo, cuando se me antoja”, es API o polling. Si es “ellos, cuando pasa algo”, es webhook.

¿Cuál es más rápido y cuál más barato? El trade-off de latencia vs eficiencia

Aquí los números, porque sin números esto es un listicle genérico.

Latencia. La latencia de polling es de hasta un intervalo completo. Si haces poll cada 30s, te enteras de un cambio hasta 30s tarde; la latencia promedio es de aproximadamente intervalo/2. Los webhooks entregan casi en tiempo real: de sub-segundo a unos pocos segundos después de que el evento se dispara. La API directa tiene cero staleness si preguntas exactamente cuando necesitas el dato.

Eficiencia. Aquí está la matemática del desperdicio. Una fuente que dispara unos 10 eventos al día, si le haces poll cada 30s, son 2,880 requests al día para atrapar 10 eventos —unas 2,870 respuestas vacías que igual pagas en llamadas y en presión de rate limit. Los webhooks entregan exactamente 10 POST. El costo del polling escala con el intervalo: cada 10s son 8,640 llamadas al día por recurso, casi todas vacías.

Pero —y este es el matiz que los posts genéricos omiten— “webhooks siempre más barato” es falso. Se invierte con volumen alto. Si una fuente emite miles de eventos por minuto, una avalancha de webhooks puede saturar tu endpoint, y un polling por lotes (“dame todo lo que cambió en el último minuto” en una sola llamada paginada) se vuelve más eficiente. La regla honesta: eventos raros favorecen webhooks; un firehose constante favorece el lote. Depende de la frecuencia de eventos contra tu frecuencia de poll, no de una preferencia.

Webhooks vs polling: el trade-off central

Webhooks (push por evento)

  • Latencia sub-segundo a segundos
  • Eficiente con eventos raros: 1 POST por evento real
  • At-least-once: SE PUEDEN perder
  • Inicia el proveedor (no controlas el timing)
  • Requiere un endpoint HTTPS público entrante

Polling (pull en agenda)

  • Latencia hasta tu intervalo completo
  • Desperdicia llamadas: 2,880/día para atrapar 10 eventos
  • Se autosana: el siguiente poll atrapa lo perdido
  • Inicias tú (controlas cadencia y reintentos)
  • Solo saliente: pasa por casi cualquier firewall
Webhooks ganan latencia y eficiencia con eventos raros; polling gana recuperabilidad y funciona detrás de un firewall.
Dimensión API (bajo demanda) Polling (programado) Webhook (push) Streaming (SSE/WS)
Latencia Cero — preguntas justo cuando lo necesitas Hasta un intervalo completo De sub-segundo a segundos Sub-segundo, continuo
Eficiencia Una llamada por necesidad real Llamadas casi vacías, gastan tu rate limit Un POST por evento real (volumen bajo) Una conexión y luego frames baratos
Fiabilidad Síncrono: ves las fallas al instante Se autosana: el siguiente poll se pone al día At-least-once: puede perderse en silencio Se cae al desconectar; hay que reconectar
Quién inicia Tú, en el momento que lo necesitas Tú, en tu propia agenda El proveedor, en su agenda Un handshake y luego el proveedor transmite
¿Endpoint entrante? No — solo saliente No — solo saliente Sí — HTTPS público entrante No — el cliente abre la conexión
Mejor para Lecturas puntuales Batch, baja urgencia, tras firewall Eventos discretos en tiempo real Feeds continuos de alta frecuencia

¿Cuándo es polling realmente la opción correcta?

El polling no es un fallback vergonzoso. Es la herramienta correcta en varios casos, y aparece otra vez como la mitad del híbrido. Lo eliges cuando:

  • La fuente no ofrece webhooks. Muy común con APIs viejas, internas o enterprise. Esto solo zanja un montón de casos reales.
  • El trabajo es por lotes o de baja urgencia: un sync nocturno, un reporte por hora, una liquidación de fin de día. Segundos de latencia son irrelevantes ahí.
  • No puedes exponer un HTTPS entrante. Firewall corporativo, on-prem, serverless sin URL estable, una laptop, localhost en dev.

La palanca del firewall: dirección de la conexión

Este es el factor que casi todos olvidan y que muchas veces decide la respuesta. Polling y API directa salen SALIENTES, y casi todo firewall ya permite tráfico saliente. Los webhooks necesitan un endpoint público entrante: la fuente se conecta hacia adentro de tu red. Detrás de un firewall estricto, en un VPC privado sin ingress, o en dev (donde necesitas un túnel como stripe listen o ngrok), simplemente no puedes recibir webhooks. Si no puedes aceptar conexiones entrantes, el polling gana por defecto, sin importar tu necesidad de latencia.

El cursor, no el updated_at ingenuo

El gotcha de implementación que produce pérdida de datos real: usa un cursor o sequence id estable (o updated_at + id como desempate), nunca updated_at > last_run a secas. El timestamp ingenuo tiene dos bugs: el clock skew entre tu máquina y la API, y filas perdidas cuando varios registros comparten el mismo timestamp en el borde de una página. Prefiere el cursor que da el servidor cuando existe (por ejemplo starting_after de Stripe). Y persiste el cursor para que un restart resuma donde se quedó.

Backoff, jitter y guardas de traslape

En errores o respuestas vacías, haz backoff exponencial. En un 429, honra Retry-After. Agrega jitter —un poco de aleatoriedad— para que muchos pollers (o muchos tenants en la misma agenda alineada) no peguen a la fuente en el mismo segundo y la saturen todos a la vez (el clásico thundering herd, la estampida de requests). Y pon una guarda de traslape: que un poll lento no arranque un segundo poll concurrente sobre el mismo recurso.

Un detalle que la gente cree que es solo cosa de webhooks: el polling también necesita idempotencia. Polls que se traslapan o se reintentan re-entregan el mismo registro. Si tu procesamiento dispara side effects, los dispara dos veces. La virtud escondida del polling compensa: se autosana. Una ventana perdida la atrapa el siguiente poll, automáticamente, porque siempre vuelve a preguntar desde el cursor.

Los números que deciden, por patrón

Latencia webhooksub-segundo a segundos
Latencia pollinghasta un intervalo completo
Polling cada 30s2,880 calls/día para 10 eventos
Webhook equivalente10 POST exactos
Fiabilidadpolling se autosana · webhook se puede perder
EsfuerzoAPI bajo · polling medio · webhook alto
Latencia, desperdicio y el perfil de fiabilidad/esfuerzo de cada patrón, lado a lado.

¿Qué sale mal con los webhooks en producción que nadie menciona?

Recibir un webhook es un endpoint de 10 líneas. Correr uno en producción no lo es. La ingeniería de fiabilidad es el costo real de los webhooks, y aquí está, con los detalles exactos —porque equivocarte en uno solo se ve en el log a las 3 a.m. Me baso en el caso de Stripe en FinHOA, pero generaliza a cualquier proveedor.

Verificación de firma sobre el body crudo

Tu URL es pública. Cualquiera que la adivine o la filtre puede hacer un POST falso de “pago exitoso”. Verifica la firma (por ejemplo Stripe-Signature) sobre el body CRUDO, los bytes tal cual llegaron, antes de hacer nada. Si tu framework parsea el JSON y lo re-serializa, el body cambia (espacios, orden de llaves) y el HMAC ya no coincide: la verificación falla. Ese es el bug número uno de “mis webhooks no jalan”. Checa también una tolerancia de timestamp (unos 5 min) para bloquear replays, y nunca implementes el HMAC a mano —usa el SDK del proveedor.

Idempotencia, en una sola transacción

La entrega es at-least-once y puede reordenarse, así que el mismo event.id llega dos o más veces (reintentos, red). Deduplica con una constraint UNIQUE sobre el id del evento y corta corto los duplicados antes de tocar el estado. Y aquí el detalle que sostiene todo: la escritura de idempotencia y la mutación de negocio deben ir en la MISMA transacción de base de datos. Si están separadas, un crash entre “marqué la factura pagada” y “registré el evento” hace que el reintento doble-provisione. La gente describe la idempotencia como “nomás checa si ya viste el id” y se salta esto. Es load-bearing.

Ack rápido, trabajo pesado async

Verifica, encola a un job en background (Sidekiq/Celery/BullMQ/SQS), regresa 200, y haz el trabajo lento de forma asíncrona. Nunca corras emails, syncs a un ERP o llamadas externas dentro del handler. El proveedor corta la espera en segundos (Stripe alrededor de 20s), y la práctica sana es responder muy por debajo de eso; si tu handler se pasa, el proveedor lo lee como falla y reintenta —amplificando carga y duplicados. El orden es: verifica → encola → 200 → procesa async.

El asesino silencioso: entregas perdidas

Aun con reintentos, algunos eventos se pierden. Tu endpoint estuvo caído más allá de la ventana de reintentos, o —peor— tu handler regresó 200 sobre una escritura que falló, y el evento se cae sin ningún error de tu lado. Específico de Stripe: la entrega es at-least-once y reintenta con backoff exponencial hasta ~72 horas (3 días), luego se rinde. Esa ventana finita es exactamente por qué la reconciliación no es opcional. Agrega una dead-letter queue para lo que falla todos los reintentos, y una alarma de “silencio = falla”: cero webhooks en N horas casi siempre significa un endpoint roto en silencio, no que no pasó nada.

¿Qué es el patrón híbrido de webhooks + reconciliación?

Aquí está el clímax y mi tesis. Los sistemas serios no eligen uno: corren webhooks como el camino rápido + un poll de reconciliación periódico como la red de seguridad, y ambos caminos comparten una sola función de sync idempotente. La idempotencia es lo que hace inofensivo el traslape: el poll y el webhook pueden entregar el mismo evento, y tu handler hace no-op sobre el duplicado.

Aquí está el patrón en pseudocódigo —es el artefacto más load-bearing del post, generalizado de la sync_stripe_to_db que corre en FinHOA:

# Camino A — tiempo real: el handler de webhooks (rápido)
@app.post("/webhooks/proveedor")
async def handle_webhook(request):
    raw = await request.body()                       # BYTES CRUDOS, nunca request.json()
    event = verify_signature(raw, request.headers["x-signature"], SECRET)  # SDK, no a mano
    if event is None:
        return Response(status=400)                  # firma inválida o body alterado
    # Encola AMBOS: event["id"] es la clave de dedup; el id del objeto es para re-jalar estado
    enqueue(event["id"], event["data"]["object"]["id"])
    return Response(status=200)                       # 2xx rápido o el proveedor reintenta

# El worker — idempotente, en UNA sola transacción
def process(event_id, resource_id):
    with db.transaction():
        if db.exists("processed_events", id=event_id):   # constraint UNIQUE sobre event_id
            return                                       # duplicado -> no-op
        sync_from_source(resource_id)                    # la MISMA función que el cron
        db.insert("processed_events", id=event_id)       # dedup + efecto, juntos

# Camino B — red de seguridad: poll de reconciliación (cron, cada 5-15 min)
def reconcile():
    cursor = load_cursor()                               # checkpoint persistido
    for event in provider.events_list(starting_after=cursor):  # SALIENTE, firewall-friendly
        # Barremos la lista de eventos solo para saber QUÉ objeto re-sincronizar;
        # la corrección la da sync_from_source, que re-jala el estado actual.
        process(event["id"], event["data"]["object"]["id"])    # mismo sync idempotente
        save_cursor(event["id"])                         # avanza el cursor por paso

# El sync compartido: el proveedor es la fuente de verdad, tu DB una réplica
def sync_from_source(resource_id):
    obj = provider.fetch(resource_id)                    # re-jala el ESTADO ACTUAL
    db.upsert(derive_local_row(obj))                     # ON CONFLICT DO UPDATE

Tres reglas de diseño lo hacen correcto:

  1. Una sola función de sync idempotente alimenta ambos caminos. En FinHOA es una sync_stripe_to_db(customer_id), llamada desde el handler de webhooks, desde el redirect de éxito Y desde el cron nocturno de reconciliación. Un solo camino de código, cero deriva entre eventos.
  2. Reconcilia contra el estado ACTUAL, no contra un replay de eventos. El cron puede barrer la lista de eventos desde el cursor (paginada y firewall-friendly) para descubrir qué objetos cambiaron, pero no reproduce el efecto del evento: usa su id solo para re-jalar el objeto y hacer upsert de su estado real. Para datos críticos, un barrido directo de objetos por status (por ejemplo, listar suscripciones activas) es aún más robusto que depender del feed de eventos. El estado es autocorrectivo; reproducir eventos perdidos no lo es.
  3. Trata la API del proveedor como fuente de verdad y tu DB como una réplica de lectura sincronizada. Tu app no calcula el estado; lo espeja.

La cadencia va por las apuestas: cada 5–15 minutos (o minutos) para datos críticos de dinero o estado; nocturno para la cola larga de lo poco urgente. ¿Por qué los reintentos del proveedor no reemplazan esto? Porque las 72 horas de reintentos de Stripe no atrapan un bug en tu handler que regresa 200 sobre una escritura fallida, ni eventos que se cayeron después de que la ventana expiró.

La línea que me llevo de todo esto, y la digo sin rodeos: una integración de webhooks sin un job de reconciliación es un incidente de pérdida de datos esperando un mal deploy. Nunca me arrepentí del cron; sí me han paginado por no tenerlo.

El pipeline híbrido: dos caminos, un solo sync idempotente

Evento se disparaEl proveedor hace POST a tu URL pública
Verifica firma (body crudo)HMAC sobre los bytes tal cual; 400 si no coincide
Deduplica + encolaevent.id con constraint UNIQUE; ack 200 rápido
Sync idempotente (async)sync(id): re-jala estado actual, upsert
En paralelo: cron de reconciliaciónCada 5-15 min lista eventos desde el cursor
Mismo sync idempotenteEl cron llama la MISMA función; el traslape es no-op
El webhook da velocidad; el cron da la garantía. Convergen en la misma función de sync.

¿Cuándo necesitas WebSockets o SSE en vez de webhooks?

El streaming merece una sección honesta, y la distinción hay que clavarla exacta —confundirlos es una señal de que el autor no sabe.

Webhooks = eventos discretos entre SERVIDORES. payment.succeeded, order.shipped. Son POST sin estado, balanceables entre muchos servidores, con reintentos. Uno por evento, perfecto a volumen bajo o medio.

Streaming = datos continuos hacia un CLIENTE sobre una conexión que se mantiene abierta: precios en vivo, dashboards, chat, presencia, cursores colaborativos, streaming de tokens de un LLM.

Dentro del streaming hay dos opciones, y SSE es el default subestimado:

  • SSE (Server-Sent Events): unidireccional servidor→cliente sobre HTTP/HTTPS plano. Se auto-reconecta con Last-Event-ID, pasa por proxies y firewalls sin drama. Es la opción correcta cuando el flujo es de una sola vía.
  • WebSocket: full-duplex de dos vías (ws/wss). No se auto-reconecta (eso lo construyes tú) y necesita sticky sessions. Elígelo solo cuando el cliente también debe empujar seguido —chat, multiplayer, edición colaborativa.

La regla: eventos discretos con minutos u horas de separación → webhook. Un firehose de updates por segundo sobre una conexión larga → streaming. Y componen: un webhook actualiza tu backend, y luego SSE abanica el cambio hacia los navegadores conectados. No pagues el costo operativo de una conexión persistente (escalado, reconexiones, sticky sessions) donde un POST de webhook sin estado bastaba.

¿Cómo decides? Un marco de decisión práctico

Aquí está la columna vertebral que viniste a buscar: las preguntas en orden de prioridad. Hazlas así y casi siempre la respuesta cae sola.

Q1 — ¿La fuente siquiera OFRECE webhooks? No → polling o API bajo demanda, listo. Esta sola pregunta zanja muchos casos reales antes que cualquier otra cosa.

Q2 — ¿Solo necesitas el dato cuando un usuario lo pide (un lookup, un render de pantalla)? → API request/response directa. No construyas infraestructura de push para un dato que lees bajo demanda; eso es sobre-ingeniería.

Q3 — ¿Necesitas reaccionar casi en tiempo real a eventos que no controlas, Y puedes exponer un HTTPS público? → webhooks. Si no hay endpoint público (firewall/serverless/on-prem) → polling, sin importar la latencia.

Q4 — ¿Qué tan fresco debe ser? Segundos → webhooks. Minutos a horas → polling es más simple y tiene menos piezas móviles. Stream continuo de alta frecuencia → SSE/WebSocket.

Q5 — ¿Qué tan grave es un evento perdido o tardío (dinero, fulfillment, acceso)? Grave → webhooks + poll de reconciliación (híbrido), siempre.

El encuadre completo en una línea: quién necesita el dato, qué tan fresco, quién puede iniciar, y cuál es el costo de un miss. El default para cualquier integración seria orientada a eventos es el híbrido; polling-solo y webhook-solo cada uno se queda con la mitad de lo que producción necesita.

El marco de decisión, en orden

  1. 1. ¿Ofrece webhooks?No → polling o API. Listo, esto zanja muchos casos.
  2. 2. ¿Solo dato bajo demanda?Sí → API request/response. No construyas push.
  3. 3. ¿Tiempo real + endpoint público?Sí → webhooks. Sin endpoint público → polling.
  4. 4. ¿Qué tan fresco?Segundos → webhooks · minutos → polling · stream → SSE/WS
  5. 5. ¿Costo de un miss?Dinero/acceso → híbrido (webhooks + reconciliación), siempre.
Pregunta en este orden. Casi siempre la respuesta cae antes de llegar al final.

Preguntas frecuentes

¿Los webhooks son más fiables que el polling? No. Son de menor latencia pero más propensos a perder eventos. El polling se autosana —el siguiente poll atrapa lo perdido—; los webhooks necesitan reintentos + reconciliación para igualar esa garantía.

¿Puedo saltarme la verificación de firma si la URL es secreta? No. Una URL adivinable o filtrada significa eventos forjados. Verifica siempre la firma sobre el body crudo; la “seguridad por oscuridad” de un endpoint no es seguridad.

¿Cada cuánto debo hacer poll? Empareja el intervalo con el staleness aceptable y los rate limits. Agrega jitter para no sincronizarte con otros pollers, y haz backoff exponencial en errores. Más frecuente = más fresco pero más caro y más 429s; no hay magia.

¿Webhook o WebSocket? Webhook = eventos discretos servidor-a-servidor, POST sin estado, balanceables. WebSocket = stream persistente de dos vías hacia un cliente. Cosas distintas; a menudo se usan juntas.

¿Sigo necesitando reconciliación si la fuente reintenta 72h? Sí. Los reintentos no cubren un handler con bug que regresa 200 sobre una escritura fallida, ni eventos que se cayeron después de que la ventana expiró. La reconciliación es la única red que atrapa esos.

¿Y si un webhook llega dos veces? Es lo esperado —la entrega es at-least-once. Deduplica por id del evento con una constraint UNIQUE, en la misma transacción que la escritura de negocio.

En resumen: cómo elegir un patrón de integración

Los cuatro patrones, mapeados a quién inicia y cuándo: jalas bajo demanda (API), jalas en agenda (polling), te empujan por evento (webhooks), o abres una tubería persistente (streaming). Eso es todo el espacio de decisión.

Y la opinión, repetida porque es la que importa: confía en los webhooks para la velocidad, nunca para la completitud. Para cualquier cosa que toque dinero o acceso, el poll de reconciliación no es negociable. Esta es exactamente la arquitectura detrás del cobro real que lancé en FinHOA —no es teoría de tutorial, es lo que aguantó en producción con dinero de verdad de por medio.

Si estás construyendo y estas integraciones son el corazón del producto, así trabajo el desarrollo de SaaS, y normalmente estas piezas viven en dashboards y herramientas internas. El patrón de webhook-como-fuente-de-verdad en la práctica es el caso trabajado a fondo, y si estás conectando agentes de IA vs herramientas de automatización, recuerda que esas herramientas dependen justo de webhooks + polling por debajo.