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}.transactionalynotif.{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.
QueuePorty el harness genérico de consumidores, que son decorey TS-002.
Comportamiento (normativo)
- Cada canal tiene exactamente dos carriles. La categoría mapea al carril:
transactionalyproductal transaccional,marketingal de marketing. - 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.
- Dos token buckets, ambos consultados: uno global por proveedor que refleja su límite real, y uno por tenant para uso justo.
- Ante un
429del proveedor, backoff, nunca descartar. Un mensaje de marketing descartado es una campaña perdida; uno transaccional descartado es un cliente que no puede entrar. - 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.
- 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.
- Todo worker es idempotente por
core.processed_jobs, y los reintentos agotados caen encore.dead_letterscon el id del envío adjunto. - 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.
- 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)
- 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. - Los workers de marketing ceden cuando la profundidad transaccional cruza el umbral, y retoman después.
- Un 429 del proveedor causa backoff y cero mensajes descartados.
- Los blasts de email usan el endpoint batch; no existe un camino de un mensaje por request.
- Un tenant que agota su bucket no consume la cuota de otro.
- Un job duplicado produce un envío.
- Los reintentos agotados caen en dead letters con el id del envío.
- Negativo: ningún camino de código escribe contactos en la feature de audiencias del proveedor.
Ejecución
Pipeline asíncrono. Workers enbackend/workers, con deployment propio por el argumento de
aislamiento de ADR-015. Buckets en Upstash, compartiendo las primitivas del rate limiting de la API.