Volver al blogMTProto

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.

Robinson Silverio25 de mayo de 20267 min de lectura
Mass messaging con MTProto: la arquitectura que aguanta en producción

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 campaignId al 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 StringSession con AES-256-GCM
  • Construye TelegramClient con apiId, apiHash, sesión
  • await client.connect()
  • Resuelve InputPeer del destino (getInputEntity(username) o getEntity(id))
  • Envía vía client.invoke(new Api.messages.SendMessage({...}))
  • En éxito: persiste log en PublicationLog con messageId real
  • 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):

  1. Marca la cuenta como pausada por N segundos en Redis
  2. Re-enqueue la tarea para otra cuenta
  3. 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_KEY rotada 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.

#mtproto#arquitectura#bullmq#telegram