Mass messaging con MTProto: la arquitectura que aguanta en producción
Workers BullMQ, account pool, lock distribuido y anti-ban. La arquitectura real que mueve millones de mensajes Telegram sin quemar cuentas.
Si quieres enviar miles de mensajes Telegram al día de forma sostenible, no es cuestión de "abrir cliente y mandar". Necesitas una arquitectura completa: cola, workers, lock, rate limit por cuenta, anti-ban, observabilidad. Este post es la versión condensada de cómo lo hace alguien que está en producción.
Si lees esto y aplicas el 60%, estás más adelantado que el 90% de los SaaS de Telegram del mercado.
Las 6 capas obligatorias
┌─────────────────────────────────────────────────┐
│ Capa 1: API / UI - Recibe órdenes │
├─────────────────────────────────────────────────┤
│ Capa 2: Queue (BullMQ + Redis) │
├─────────────────────────────────────────────────┤
│ Capa 3: Workers (Node procesos) │
├─────────────────────────────────────────────────┤
│ Capa 4: Account Pool + Lock distribuido │
├─────────────────────────────────────────────────┤
│ Capa 5: MTProto Client (GramJS) │
├─────────────────────────────────────────────────┤
│ Capa 6: Anti-ban / Rate limit / Observability │
└─────────────────────────────────────────────────┘
Cada capa resuelve un problema distinto. Si te saltas una, vas a llegar al cuello de botella correspondiente cuando escales.
Capa 1: API que crea tareas
La UI o API recibe la orden: "envía esta campaña a estos 5,000 destinos". Lo que hace:
- Valida la entrada (mensaje válido, destinos en formato correcto, no excede plan)
- Persiste la campaña en DB con estado
PENDIENTE - Enqueue N jobs en BullMQ, uno por destino (o por chunks de 10-50)
- Devuelve
campaignIdal usuario inmediatamente (la app no espera el envío)
Esto separa decisión de negocio (validación) de ejecución (envío). El usuario ve "campaña creada" en 200ms aunque enviar tome horas.
Capa 2: Queue con BullMQ + Redis
BullMQ es el estándar en Node para este caso. Características que necesitas:
- Retries con backoff: si una tarea falla, reintenta con espera exponencial (1s, 5s, 30s, 5min)
- Priorización: tareas urgentes (1:1 con cliente) saltan delante de broadcasts
- Concurrencia por cola: cada cola tiene su propio límite de workers paralelos
- Rate limiter integrado: BullMQ tiene
limiter: { max, duration }para no saturar - Dead letter queue: tareas que fallaron 5 veces van a una cola de inspección manual
Estructura recomendada de colas:
broadcast:high # urgente, prioridad 1
broadcast:normal # estándar, prioridad 2
warming # warming de cuentas nuevas
story:publish # publicación de Stories
sync:groups # sync de listas de grupos
inbox:listen # escucha mensajes entrantes
Cada cola tiene su semántica y su rate limit.
Capa 3: Workers Node especializados
Cada cola tiene un worker process dedicado (no thread, proceso). Beneficios:
- Si un worker se cae, no tumba los demás
- Puedes escalar horizontalmente (3 procesos para
broadcast:normal) - Memory leak en un worker no afecta a otros
- Despliegues progresivos: actualizas worker por worker
En Vega Punk corremos 6 workers especializados:
// workers/start-all.ts
import "./broadcast";
import "./inbox-listener";
import "./story-publisher";
import "./warming-planner";
import "./warming-session";
import "./mass-check";Cada uno suscrito a su cola, con su lógica.
Capa 4: Account Pool + Lock distribuido
Aquí está el corazón del sistema. El worker necesita decidir qué cuenta usar para esta tarea.
Account Pool
Lista de cuentas activas para el usuario, ordenadas por:
- Health score (sin FloodWait, sin errores recientes)
- Capacidad restante hoy (mensajes/día - mensajes enviados)
- Antigüedad y nivel (cuentas más maduras pueden manejar más volumen)
- Última vez usada (round robin para distribuir carga)
async function elegirCuenta(userId: string): Promise<Account | null> {
return db.account.findFirst({
where: { userId, status: "ACTIVE", capacidadRestante: { gt: 0 } },
orderBy: [
{ healthScore: "desc" },
{ ultimoEnvio: "asc" }
],
});
}Lock distribuido
Una vez elegida la cuenta, toma el lock con Redis (detalle aquí). Esto garantiza que ningún otro worker está usando esa cuenta al mismo tiempo.
return conLockConexionMTProto(account.id, async () => {
const client = await abrirCliente(account);
await client.invoke(new Api.messages.SendMessage({ ... }));
await client.disconnect();
});Sin lock, AUTH_KEY_DUPLICATED. Con lock, todo fluye.
Capa 5: MTProto Client (GramJS)
Aquí el código real de envío:
- Descifra
StringSessioncon AES-256-GCM - Construye
TelegramClientconapiId,apiHash, sesión await client.connect()- Resuelve
InputPeerdel destino (getInputEntity(username)ogetEntity(id)) - Envía vía
client.invoke(new Api.messages.SendMessage({...})) - En éxito: persiste log en
PublicationLogconmessageIdreal - En error: clasifica y propaga
Crítico: await client.disconnect() en finally. Conexiones huérfanas son la causa de AUTH_KEY_DUPLICATED.
Capa 6: Anti-ban, rate limit, observability
Rate limit por cuenta
Cada cuenta tiene su propio límite diario según su nivel progresivo. Antes de enviar, el worker consulta el contador. Si excede, omite la tarea y pasa a otra cuenta.
FloodWait handling
Si Telegram devuelve FloodWait(seconds=N):
- Marca la cuenta como pausada por N segundos en Redis
- Re-enqueue la tarea para otra cuenta
- No reintentes la misma cuenta hasta que pase el tiempo
Backoff inteligente: si recibe 3 FloodWait en 1 hora, la cuenta entra en cooldown de 24h automáticamente.
Jitter en ritmo de envío
Si envías exactamente 1 mensaje cada 5 segundos, eso es detectable como bot. Añade jitter aleatorio:
const baseMs = 5000;
const jitter = Math.random() * 2000 - 1000; // ±1000ms
await sleep(baseMs + jitter);Esto simula tiempo humano variable.
Observability
Métricas mínimas:
- Mensajes enviados/hora por cuenta y total
- FloodWait actuales (contador en tiempo real)
- AUTH_KEY_DUPLICATED por hora (idealmente 0)
- Latencia p95 por cola
- Cuentas en estado degradado
Stack típico: Prometheus + Grafana, o si prefieres SaaS, Datadog. Cualquiera funciona.
Anti-patterns clásicos que vas a querer evitar
"Un mega worker que hace todo"
Mal. Concentras el riesgo, escalas mal, y un bug en Stories tumba broadcasts.
"Lock con setTimeout en memoria del worker"
Mal. El lock vive solo en ese worker. Si tienes 4 workers, no hay coordinación. Resultado: AUTH_KEY_DUPLICATED.
"Reintentar inmediatamente en error"
Mal. Multiplicas el problema. Usa backoff exponencial siempre.
"Sesiones en variable de entorno"
Mal. ENV vars no rotan, se loguean fácilmente, no escalan. DB cifrada es el camino.
"Sin observability"
Mal. Vas a ciegas. Cuando explote a las 100 cuentas, no vas a saber por qué.
El stack en una página
Resumen de tecnologías:
- Backend: Node 20+ (TypeScript)
- Queue: BullMQ 5+ con Redis 7+
- DB: PostgreSQL 15+ con Prisma ORM
- MTProto: GramJS última versión (layer 198 al momento)
- Lock: Redis SET + EX + NX (TTL 30s)
- Cifrado: AES-256-GCM con
ENCRYPTION_KEYrotada cada 90 días - Workers: procesos PM2 o Docker containers
- Monitoring: Prometheus + Grafana o Datadog
- Proxies: residenciales (Bright Data, IPRoyal, etc.)
Cualquiera de estos componentes puede sustituirse, pero el rol es el mismo.
El tiempo real de implementación
Construir esta arquitectura desde cero con un senior dev: 6-9 meses. Si te saltas anti-ban, 3 meses para tener algo funcional pero frágil. Si te saltas observability, 4 meses pero vas a estar en deuda.
Si tu equipo no tiene MTProto explícito como skill, suma 3 meses de aprendizaje del protocolo y de Telegram. Total realista: 9-12 meses de un senior.
Por eso plataformas como Vega Punk existen. La inversión está hecha; tu producto se beneficia.
La pregunta correcta para tu CTO
"Para construir mass messaging Telegram, ¿qué hacemos: build o buy?"
Si build:
- 9-12 meses de un senior + infra mensual + mantenimiento permanente
- Probablemente quemen 50-200 cuentas en el proceso de aprender anti-ban
- Vendes tu propio servicio o usas internamente
Si buy:
- Integración API con un proveedor en 1-2 semanas
- Costo mensual proporcional al uso
- Ahorras 6-12 meses y los baneos del proceso
No hay respuesta correcta universal. Si Telegram es core de tu propuesta de valor, build empieza a tener sentido a partir del año 2. Si Telegram es un canal entre muchos, buy siempre tiene sentido.
Cierre
La arquitectura de mass messaging MTProto es uno de los problemas más subestimados en el mundo SaaS. Verse fácil engaña: "es solo enviar mensajes". En realidad son 6 capas, cada una con sus muros, cada una con su tiempo de desarrollo.
Si decides construirla, esta guía te ahorra meses. Si decides comprarla, ya sabes dónde.