Tema representativo de entrevista

Entrevista de Backend: ¿Cómo recibir y procesar Webhooks de forma segura?

BackendDifícil
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Una integración de pagos recibe webhooks de múltiples proveedores. El tráfico pico es de 2,000 solicitudes por segundo; el cuerpo promedio es de 10 KiB y el máximo es de 1 MiB. Los proveedores pueden reintentar, duplicar, retrasar y desordenar eventos. El receptor debe responder en menos de 2 segundos, verificar la autenticidad, admitir la rotación de secretos y garantizar que los eventos aceptados no se pierdan ni se apliquen dos veces. Diseñe el ingreso (ingress), el límite de aceptación duradera, el procesamiento asíncrono, la idempotencia, la reconciliación, el manejo de fallas y las pruebas.

Problema y Casos de Uso

El receptor convierte una solicitud HTTP no confiable en un evento interno duradero. La parte difícil es el límite entre esos estados. Un 200 OK rápido es incorrecto si el proceso puede fallar antes de guardar el evento. Ejecutar el flujo de trabajo de pago antes de responder también es incorrecto porque una dependencia lenta provoca reintentos del proveedor y amplifica la carga.

Utilice estos supuestos para la entrevista:

  • El tráfico pico es de 2,000 solicitudes por segundo. El cuerpo en crudo promedio es de 10 KiB y el máximo es de 1 MiB, por lo que el ingreso de cuerpos promedio en el pico es de aproximadamente 19.5 MiB/s antes de encabezados, replicación y sobrecarga de almacenamiento.
  • El proveedor espera una respuesta en menos de 2 segundos. Nuestro objetivo interno es una confirmación p99 de 500 ms para preservar margen de maniobra.
  • La entrega es al menos una vez (at-least-once) y sin orden garantizado. Un proveedor puede enviar el mismo evento lógico de forma concurrente, reintentarlo más tarde o entregar primero el estado más reciente de un objeto.
  • Un evento aceptado debe sobrevivir a una caída del receptor y finalmente alcanzar un estado terminal PROCESSED o FAILED. Un evento lógico no debe aplicar la misma mutación de negocio dos veces.
  • Los secretos de firma rotan sin tiempo de inactividad. Los payloads en crudo se cifran y se retienen durante 30 días para recuperación y auditoría; los registros de idempotencia permanecen al menos durante la ventana de reentrega documentada por el proveedor.

Los casos de uso principales son actualizaciones de estado de pagos, cambios en el ciclo de vida de suscripciones, reembolsos, disputas y notificaciones de cuentas. El diseño debe funcionar entre proveedores sin asumir que sus encabezados, algoritmos de firma, identidades de reintento o reglas de marcas de tiempo sean idénticos.

Qué está evaluando el entrevistador

Primero, el entrevistador busca un contrato de confirmación preciso. 2xx significa "este receptor aceptó el evento de forma duradera", no "se completaron todos los efectos secundarios posteriores". Devolver éxito antes de la escritura duradera crea pérdidas silenciosas. Devolver éxito para un duplicado ya aceptado es correcto porque el proveedor puede dejar de reintentar.

Segundo, buscan seguridad en el orden correcto. El receptor limita el método, tipo de contenido, encabezados y tamaño del cuerpo; preserva los bytes en crudo exactos; verifica la firma específica del proveedor con versiones de secretos confiables; compara MACs en tiempo constante; y valida los metadatos de frescura firmados cuando el proveedor los suministra. Parsear y volver a serializar JSON antes de la verificación puede alterar espacios en blanco o el orden de las claves e invalidar una firma legítima.

Tercero, quieren que el candidato separe la prevención de reproducción (replay) de la deduplicación de reintentos. Una marca de tiempo firmada rechaza una solicitud capturada antigua. Un ID de entrega o de evento estable del proveedor evita que un reintento válido se aplique dos veces. Algunos proveedores generan una nueva marca de tiempo y firma de intento para cada reintento mientras retienen el ID de evento lógico. Un mecanismo no puede reemplazar al otro de forma segura.

Cuarto, buscan un modelo de procesamiento duradero sin el hueco de doble escritura entre base de datos y cola. Una fila de inbox y una fila de outbox en la base de datos se pueden confirmar juntas en una transacción; luego, un relay publica el trabajo. Alternativamente, los workers pueden arrendar (lease) filas del inbox directamente. La clave única se aplica a nivel de almacenamiento, no mediante una verificación de lectura previa a la escritura vulnerable a duplicados concurrentes.

Finalmente, una respuesta sólida maneja estados fuera de orden, efectos secundarios externos, rotación de secretos, eventos envenenados (poisoned), contrapresión (backpressure), observabilidad, reconciliación y caídas en cada límite transaccional.

Preguntas de aclaración antes de responder

  • ¿Qué promete exactamente 2xx? Aquí significa que la firma pasó y que el evento, o su duplicado previamente aceptado, es duradero. No promete que los correos electrónicos, el libro mayor o las llamadas a la API del proveedor hayan finalizado.
  • ¿Qué identidad del proveedor es estable a través de los reintentos? Cada adaptador debe documentar el ID de evento lógico, la marca de tiempo del intento, el formato de firma y si la reentrega manual preserva el mismo ID. Nunca derive la identidad únicamente del hash del payload.
  • ¿El proveedor firma el cuerpo en crudo y los metadatos? El adaptador define los bytes canónicos firmados. El framework HTTP debe exponer el cuerpo intacto antes de que se ejecute el middleware de JSON.
  • ¿Qué información de ordenamiento existe? Prefiera una versión o secuencia de objeto autoritativa. La hora de creación de un evento es evidencia útil pero no constituye automáticamente un orden estricto. Cuando no existe una versión, consulte el estado actual del proveedor para eventos que definen estado.
  • ¿Durante cuánto tiempo puede ocurrir la reentrega? La retención de idempotencia y la superposición de secretos antiguos deben cubrir el comportamiento documentado del proveedor y la política de reintento manual del producto. La retención de eventos en crudo de 30 días en este problema es un supuesto de producto, no una regla universal de los proveedores.
  • ¿Qué fallas deberían provocar un reintento? Si no se puede establecer la autenticidad o el almacenamiento duradero no está disponible, no acuse recibo. Después de la aceptación duradera, las interrupciones en los workers no deben alterar la respuesta HTTP.
  • ¿Qué datos son sensibles? Cifre los cuerpos en crudo, restrinja el acceso, censure (redact) los logs y defina excepciones de eliminación. Una firma verifica autenticidad e integridad; no cifra el payload.

Estructura de respuesta en 30 segundos

“Expondría un endpoint HTTPS específico por proveedor detrás de límites de tasa y tamaño de cuerpo, capturaría los bytes en crudo exactos y verificaría el ID firmado, la marca de tiempo y el payload con el secreto actual o anterior usando comparación en tiempo constante. Dentro de una sola transacción de base de datos, insertaría una fila en el inbox bajo una clave única del evento del proveedor y una fila en el outbox, devolviendo luego 2xx; un duplicado ya aceptado también recibe 2xx. Un relay y workers procesan de forma asíncrona mediante leases y reintentos. La mutación de negocio y el marcador de procesado se confirman juntos, mientras que los efectos externos usan un outbox y una clave de idempotencia estable. Para entregas sin orden uso versiones de objetos o consulto el estado actual autoritativo, nunca el orden de llegada. Monitorearía la latencia de confirmación, motivos de rechazo, antigüedad del inbox, duplicados y fallas, y probaría duplicados concurrentes, superposición de secretos, firmas obsoletas, eventos fuera de orden y caídas alrededor de cada confirmación transaccional.”

Análisis detallado paso a paso

Asigne a cada proveedor un adaptador, pero mantenga un único pipeline de recepción. El adaptador suministra los tipos de eventos permitidos, el tamaño máximo de cuerpo, el parseo de encabezados, los bytes firmados canónicos, los algoritmos, las versiones de secretos confiables, la política de frescura y la extracción del ID de evento lógico. Los secretos provienen de un gestor de secretos administrado y se almacenan en caché solo durante un período acotado. Una solicitud nunca debe elegir su propia clave de verificación a través de un encabezado no confiable.

La secuencia de ingreso es deliberada:

text
1. Require HTTPS POST; apply endpoint and provider rate limits.
2. Validate bounded headers and Content-Length when present.
3. Read at most 1 MiB into raw bytes; reject overflow while streaming.
4. Parse signature metadata without parsing the JSON body.
5. Verify current and previous trusted secret versions in constant time.
6. Check the signed attempt timestamp against the provider-specific tolerance.
7. Parse the verified body and validate the event envelope and allowed type.
8. Durably accept under a unique logical-event key, then acknowledge.

La frescura de la marca de tiempo y la deduplicación resuelven ataques diferentes. Supongamos que un atacante captura una solicitud firmada válida. Una ventana estrecha de marca de tiempo firmada bloquea la reproducción después de la ventana, pero la misma solicitud capturada aún puede llegar dos veces dentro de ella. A la inversa, un registro de ID de evento bloquea un evento lógico duplicado pero no puede probar que una marca de tiempo sin firmar sea fresca. Los proveedores también difieren: los reintentos pueden llevar una nueva marca de tiempo de intento firmada mientras conservan el mismo ID de evento. Conserve ambas verificaciones y haga que su semántica exacta dependa del adaptador.

Utilice un inbox como fuente de verdad:

text
WebhookInbox(
  inbox_id, provider, endpoint_id, provider_event_id,
  event_type, object_id, object_version, provider_created_at,
  received_at, raw_payload_ref, payload_hash, matched_secret_version,
  status, attempt_count, next_attempt_at, lease_until, last_error
)

WebhookOutbox(outbox_id, inbox_id, topic, created_at, published_at)

UNIQUE(provider, endpoint_id, provider_event_id)

Dentro de una única transacción de base de datos, inserte la fila del inbox y su notificación de outbox. Si la clave única ya existe, lea su estado de aceptación y devuelva 2xx sin crear más trabajo. Esta es una inserción atómica, no un "consultar y luego insertar". Confirme la transacción antes de acusar recibo. Si la base de datos no está disponible o el resultado de la confirmación es desconocido, devuelva un código no 2xx reintentable; un duplicado posterior convergerá en la fila única si la primera confirmación realmente tuvo éxito.

El diseño de base de datos más outbox cierra la brecha entre guardar y encolar. Un relay publica repetidamente las filas no publicadas del outbox y las marca como publicadas. La publicación puede ocurrir dos veces, por lo que los consumidores de la cola aún deduplican mediante inbox_id. Una implementación más simple puede omitir el broker y permitir que los workers tomen las filas pendientes del inbox mediante leases, como FOR UPDATE SKIP LOCKED. Elija según el rendimiento requerido y las necesidades operativas, pero mantenga el inbox como el límite de auditoría y aceptación duradera.

Los workers toman un lease corto, parsean el evento versionado y enrutan solo los tipos admitidos. Para una mutación en la misma base de datos, actualice la fila de negocio, registre el evento procesado y marque el inbox como PROCESSED en una sola transacción. La tabla de eventos procesados tiene la misma clave estable del proveedor, por lo que un reintento del worker se convierte en una operación no-op. Para llamadas a otro servicio, escriba una entrada local de outbox con inbox_id como su clave de idempotencia. La entrega exactamente una vez (exactly-once) a través de redes arbitrarias sigue siendo imposible; una identidad estable y receptores idempotentes hacen que la ejecución al menos una vez (at-least-once) sea segura.

El orden de llegada no puede definir el orden de negocio. Si los eventos llevan una versión de objeto autoritativa, actualice con una condición como incoming_version > stored_version; los eventos obsoletos se convierten en no-ops procesados. Si solo existen notificaciones que fijan estado, consulte el recurso actual del proveedor y converja el estado local. Si el evento representa un delta no repetible, exija una secuencia, almacene en búfer una brecha acotada y reconcilie las versiones faltantes. Una marca de tiempo por sí sola puede empatar, desfasarse o describir el momento de creación en lugar del orden de confirmación.

Las fallas se dividen en el límite duradero. Antes de la aceptación, firmas inválidas, marcas de tiempo firmadas obsoletas, cuerpos excesivamente grandes, secretos no disponibles y almacenamiento no disponible producen un rechazo o una respuesta reintentable según la política. No persista cuerpos sensibles no verificados simplemente para depurarlos. Después de la aceptación, las interrupciones en la cola o en los workers siguen recibiendo 2xx; los registros del inbox se acumulan y la recuperación los drena. Las fallas transitorias de los workers aplican backoff con jitter. Los errores de esquema y los reintentos agotados entran en FAILED, preservan un diagnóstico censurado y activan una ruta de recuperación visible para los operadores.

La rotación de secretos mantiene las versiones actual y previa como confiables durante un período de superposición acotado, derivado del comportamiento de reentrega del proveedor. Registre qué versión coincidió, pero nunca guarde en logs el secreto ni la firma. Los nuevos secretos se configuran en ambos extremos, se observan en producción y las versiones antiguas se retiran explícitamente. Un compromiso de seguridad de emergencia puede requerir el retiro inmediato y la reproducción desde el proveedor, por lo que el manual operativo (runbook) de rotación debe distinguir la superposición planificada de la respuesta a incidentes.

A 2,000 solicitudes/s y un promedio de 10 KiB, el ingreso ve aproximadamente 19.5 MiB/s de cuerpos en crudo. La planificación de capacidad incluye CPU para TLS y HMAC, tasa de transacciones de base de datos, replicación, amplificación en colas y duración de ráfagas. Escale el ingreso sin estado horizontalmente, particione los índices del inbox por proveedor y tiempo si es necesario, mantenga la clave única exigible globalmente dentro de su límite de propiedad y coloque los cuerpos en crudo cifrados en almacenamiento de objetos cuando las filas de la base de datos se vuelvan demasiado grandes.

Mida por separado las tasas de eventos aceptados, duplicados, con firma inválida, obsoletos, con sobretamaño y no admitidos. Rastree la latencia de confirmación p50/p95/p99, latencia de confirmación en base de datos, antigüedad del registro sin procesar más antiguo en el inbox, tasas de éxito y reintento de workers, recuentos de FAILED, retraso del relay de outbox y versiones de secretos coincidentes. Las alertas deben usar el retraso y el estado duradero, no solo la profundidad de la cola. Un trabajo de reconciliación compara las filas aceptadas del inbox con los registros procesados y la publicación del outbox, reenviando a la cola el trabajo faltante seguro.

Pruebe desde los bytes HTTP en crudo hacia adentro. Utilice vectores de firma publicados por el proveedor, luego altere un byte, un espacio en blanco, el ID o la marca de tiempo. Pruebe encabezados faltantes y duplicados, límites de tamaño de cuerpo, desfase de reloj, secretos actuales/anteriores y retiro de claves. Envíe cientos de copias concurrentes de un solo evento y demuestre que resulta en una sola fila de inbox y una sola mutación de negocio. Simule caídas después de la confirmación del inbox pero antes de la respuesta, después de publicar en la cola pero antes de marcar el outbox, y después de la confirmación de negocio pero antes del acuse de recibo del worker. Entregue las versiones 3, 1 y 2; sature los workers; restablézcalos; y demuestre convergencia eventual con latencia de confirmación acotada.

Respuesta de ejemplo de alta calidad

“Defino 2xx como aceptación duradera. El ingreso es sin estado y específico del proveedor únicamente en el límite del adaptador. Acepta HTTPS POST, limita el cuerpo a 1 MiB, preserva los bytes en crudo exactos y verifica el ID firmado canónico del proveedor, la marca de tiempo de intento y el payload contra secretos confiables actuales o anteriores. Las comparaciones HMAC son en tiempo constante. Luego parseo el sobre verificado y permito únicamente tipos de eventos admitidos.

En una sola transacción de base de datos inserto WebhookInbox bajo UNIQUE(provider, endpoint_id, provider_event_id) e inserto una fila en el outbox. Acuso recibo solo después del commit. Un duplicado concurrente entra en conflicto con esa clave y también recibe 2xx sin generar nuevo trabajo. A 2,000 solicitudes por segundo y un promedio de 10 KiB, el ingreso en crudo es de aproximadamente 19.5 MiB/s, por lo que escalo el ingreso horizontalmente y dimensiono la CPU de firmas, commits de base de datos, replicación y almacenamiento para ráfagas en lugar de solo contar solicitudes.

Un relay de outbox publica inbox_id; la publicación duplicada es segura. Los workers toman en lease el registro del inbox. La mutación de negocio, el marcador de evento procesado y la finalización del inbox comparten una transacción cuando es posible. Los efectos secundarios remotos utilizan otro outbox y inbox_id como clave de idempotencia. Esto evita asumir una entrega exactamente una vez a través de una red, garantizando al mismo tiempo que los reintentos no repitan el efecto lógico.

No utilizo el orden de llegada. Una versión de objeto autoritativa condiciona las actualizaciones; de lo contrario, los webhooks que fijan estado activan una lectura del estado actual del proveedor. Las brechas de secuencia faltantes entran en reconciliación. Antes de la aceptación duradera, una solicitud no verificable o una caída del almacenamiento no se confirma. Después de la aceptación, una interrupción en los workers es absorbida por el inbox y sigue recibiendo 2xx.

Roto los secretos con una superposición acotada de versiones actual/anterior y registro la versión coincidente. Monitoreo la latencia de confirmación, clases de rechazo, duplicados, antigüedad del inbox, retraso del outbox, fallas y uso de versiones de secretos. Finalmente, pruebo vectores de firma oficiales, mutaciones de bytes, marcas de tiempo obsoletas, superposición de secretos, duplicados concurrentes, versiones fuera de orden y caídas antes y después de cada commit. La condición de aprobación es una fila duradera y una mutación de negocio por evento lógico, sin pérdida confirmada y convergencia eventual tras la recuperación.”

Errores comunes

  • Parsear JSON antes de la verificación → la reserialización altera los bytes firmados → capture y verifique primero el cuerpo en crudo exacto.
  • Devolver 200 antes de la persistencia → una caída pierde silenciosamente un evento confirmado → haga commit en el inbox antes de responder.
  • Guardar en la base de datos y luego publicar una sola vez → una caída entre escrituras deja trabajo varado → haga commit de un outbox junto con el inbox o arriende filas del inbox directamente.
  • Verificar duplicados con una lectura previa → las solicitudes concurrentes pasan ambas → aplique una clave única de evento de proveedor de forma atómica.
  • Usar solo frescura de marca de tiempo → duplicados simultáneos aún se aplican dos veces → combine frescura con deduplicación por ID estable.
  • Usar solo un ID de evento → una solicitud válida capturada se puede reproducir mientras su registro esté ausente o expirado → verifique también la frescura firmada según el contrato del proveedor.
  • Asumir que el orden de llegada es el orden del evento → eventos tardíos revierten el estado hacia atrás → use versiones, transiciones monotónicas o reconciliación con el estado autoritativo.
  • Ejecutar efectos secundarios remotos en el manejador HTTP → la latencia provoca reintentos y resultados inciertos → acepte de forma duradera y luego use trabajo asíncrono idempotente.
  • Guardar en logs payloads completos y firmas → la observabilidad se convierte en una fuga de datos y secretos → almacene evidencia cifrada con acceso restringido y registre identificadores censurados.

Preguntas de seguimiento y respuestas

Pregunta de seguimiento 1: ¿Por qué devolver 2xx para un duplicado cuyo procesamiento no ha finalizado?

El evento original ya fue aceptado de forma duradera, por lo que otro reintento del proveedor no aporta valor de recuperación. Devolver una falla crearía más tráfico duplicado. La fila existente en el inbox sigue siendo elegible para los workers y la reconciliación. Esto es seguro únicamente si la fila es duradera y no se encuentra en un estado que indique que la aceptación fue revertida.

Pregunta de seguimiento 2: ¿Qué sucede si el proceso falla después de confirmar el inbox pero antes de enviar 2xx?

El proveedor reintenta. La clave única encuentra la fila confirmada, no se crea un segundo trabajo y el receptor devuelve 2xx. Esta es la ruta esperada de al menos una vez (at-least-once). Si el cliente recibe 2xx pero el resultado de la conexión es ambiguo para el proveedor, aplica la misma convergencia.

Pregunta de seguimiento 3: ¿Puede Redis almacenar las claves de idempotencia?

Puede actuar como un acelerador, pero una caché de corta duración por sí sola es más débil que el contrato de aceptación. El desalojo (eviction), la conmutación por error (failover) o la expiración podrían permitir que el mismo evento de pago se aplique nuevamente. Mantenga la identidad procesada duradera durante la ventana de negocio y reentrega requerida; use Redis solo cuando la pérdida de la clave no pueda violar ese contrato.

Pregunta de seguimiento 4: ¿Cómo maneja el tiempo de inactividad del almacén de secretos?

Utilice una caché en memoria cifrada y acotada de versiones ya confiables con expiración explícita y métricas. Si no existe una clave válida en caché, falle de forma cerrada (fail closed) y devuelva una respuesta reintentable para que el proveedor pueda reenviar. Nunca acepte trabajo sin firmar ni obtenga un identificador de clave de una solicitud no confiable confiando en él automáticamente.

Pregunta de seguimiento 5: ¿Cómo recupera un flujo de pagos fuera de orden?

Prefiera una versión de objeto del proveedor o una transición de dominio monotónica y rechace la regresión de estado. Si el evento solo indica que un objeto cambió, consulte su estado autoritativo actual. Para deltas secuenciados, almacene en búfer una brecha acotada, solicite las versiones faltantes y genere una alerta cuando la brecha supere la ventana de recuperación. No ordene basándose únicamente en la hora de recepción local.

Pregunta de seguimiento 6: ¿Por qué esto sigue sin ser exactamente una vez (exactly-once)?

El receptor puede hacer que su mutación local y su marcador de procesado sean atómicos. No puede confirmar de forma atómica junto con un servicio remoto arbitrario de correo electrónico, banco o proveedor. Una falla de red puede ocultar si el lado remoto aplicó una solicitud. Una clave de idempotencia estable, un outbox transaccional, reintentos y reconciliación ofrecen un resultado de negocio efectivamente una vez donde la API remota admite idempotencia, mientras que el contrato de transporte sigue siendo al menos una vez (at-least-once).

Fuentes públicas

Preguntas relacionadas