Skip to main content
Traducción. Autoritativo: fs-msg-0005-dual-rail-dispatch.md.

Contexto

Esta feature existe por un hallazgo concreto: el rate limit de Resend es por team, aplicado a todas las API keys — todos los tenants comparten un presupuesto. Sin aislamiento, un blast de marketing deja sin OTPs, invitaciones ni comprobantes a todos los demás. Una plataforma donde la campaña de un cliente rompe el login de otro no es multi-tenant en ningún sentido que importe. La respuesta de ADR-015 son dos carriles por canal con prioridad estricta, más token buckets en dos niveles: uno global por proveedor que refleja su límite real, y uno por tenant para equidad. El global es física; el por tenant es justicia, y se necesitan ambos.

Alcance (normativo)

  • Dos colas por canal: notif.{canal}.transactional y notif.{canal}.marketing.
  • Harness de worker sobre la infraestructura compartida de idempotencia y reintentos (FS-CORE-0007).
  • Token buckets en Upstash: global por proveedor, y por tenant.
  • Workers de marketing que ceden cuando la profundidad transaccional cruza el umbral.
  • Despacho por lotes para email, usando el endpoint batch del proveedor.
  • Contrapresión y profundidad de cola observable por carril.

Fuera de alcance (normativo)

  • Los adaptadores — FS-MSG-0006. El despacho decide cuándo y a qué velocidad; los adaptadores entregan.
  • Decidir si enviar — FS-MSG-0001.
  • QueuePort y el harness genérico de consumidores, que son de core y TS-002.

Comportamiento (normativo)

  1. Cada canal tiene exactamente dos carriles. La categoría mapea al carril: transactional y product al transaccional, marketing al de marketing.
  2. El transaccional siempre gana. Cuando su profundidad cruza el umbral, los workers de marketing ceden — terminan el job en curso y dejan de tomar hasta que la profundidad se recupere. Ceder, no drenar: un worker de marketing que muere pierde su progreso.
  3. Dos token buckets, ambos consultados: uno global por proveedor que refleja su límite real, y uno por tenant para uso justo.
  4. Ante un 429 del proveedor, backoff, nunca descartar. Un mensaje de marketing descartado es una campaña perdida; uno transaccional descartado es un cliente que no puede entrar.
  5. Los blasts de email usan exclusivamente el endpoint batch del proveedor (100 por request en Resend). Enviar un blast de a un request agota el bucket global para todos.
  6. Los contactos nunca se sincronizan a la feature de audiencias del proveedor. Nuestro almacén es la fuente de verdad; una copia allá es un segundo registro de consentimiento que no gobernamos.
  7. Todo worker es idempotente por core.processed_jobs, y los reintentos agotados caen en core.dead_letters con el id del envío adjunto.
  8. La profundidad de cola por carril es una métrica publicada con alerta. Un carril transaccional que crece es un incidente, no una estadística.
  9. PROHIBIDO: un job de marketing en el carril transaccional · saltarse los buckets por un envío “urgente” · descartar un mensaje ante un 429.

Datos (normativo)

API (normativo)

De cara a Ops. Un tenant no ajusta nuestros rate limits.

Eventos (normativo)

Ninguno. El despacho es mecanismo; los eventos de envío (FS-MSG-0003) son el registro.

Criterios de aceptación (normativo)

  1. El perfil k6 blast-vs-otp: un blast a máximo throughput mientras los envíos transaccionales de un segundo tenant mantienen su presupuesto de latencia. Es el criterio que justifica toda la feature.
  2. Los workers de marketing ceden cuando la profundidad transaccional cruza el umbral, y retoman después.
  3. Un 429 del proveedor causa backoff y cero mensajes descartados.
  4. Los blasts de email usan el endpoint batch; no existe un camino de un mensaje por request.
  5. Un tenant que agota su bucket no consume la cuota de otro.
  6. Un job duplicado produce un envío.
  7. Los reintentos agotados caen en dead letters con el id del envío.
  8. Negativo: ningún camino de código escribe contactos en la feature de audiencias del proveedor.

Ejecución

Pipeline asíncrono. Workers en backend/workers, con deployment propio por el argumento de aislamiento de ADR-015. Buckets en Upstash, compartiendo las primitivas del rate limiting de la API.

Preguntas abiertas

Changelog

Registro de entrega

Aún no implementado.