Sharding en bots de Discord: cómo escalar a miles de servidores
Qué es un shard, cuándo obliga Discord a usar sharding y cómo escalar un bot de Discord: caché, estado en Redis y PostgreSQL, colas, rate limits y despliegues.
Escalar un bot de Discord consiste sobre todo en dos cosas: repartir los servidores entre varias conexiones al gateway (sharding) y sacar el estado fuera del proceso para que esas conexiones puedan vivir en máquinas distintas. En este artículo te explico qué es un shard, cuándo lo exige Discord, cómo organizar procesos y caché, y qué haría para que el bot aguante el crecimiento sin caídas.
Escribo desde la experiencia de mantener CactusFire desde 2019. No voy a describir su arquitectura interna; te cuento los principios que aplico y lo que aprendí por el camino.
Qué es un shard en Discord
Un bot recibe los eventos (mensajes, miembros que entran, interacciones) a través de una conexión WebSocket con el gateway de Discord. Un shard es una de esas conexiones, y cada una se encarga de un subconjunto de servidores.
Discord decide a qué shard va cada servidor con una fórmula fija:
shard_id = (guild_id >> 22) % num_shards
Esto tiene una consecuencia práctica: un shard solo conoce los servidores que le tocan. Si un comando necesita datos de otro servidor, ese dato no está en la memoria de tu proceso.
Cuándo es obligatorio el sharding
La documentación de Discord es clara en dos números:
- Un bot en 2.500 servidores o más tiene que usar sharding.
- Cada shard admite como máximo 2.500 servidores.
La guía de discord.js recomienda empezar a prepararlo hacia los 2.000 servidores y apunta a unos 1.000 servidores por shard. Puedes pedir a Discord el número recomendado con el endpoint GET /gateway/bot, que devuelve cuántos shards usar y tus límites de sesión.
Cuántos servidores por shard: el caso de CactusFire
CactusFire publica sus cifras: más de 295.000 servidores y 304 shards. Si divides, salen unos 970 servidores por shard, en línea con la recomendación de la guía.
¿Por qué no llenar cada shard hasta 2.500? Porque cada shard lleva su caché, sus eventos y su carga de CPU. Shards más pequeños reparten mejor el trabajo, se recuperan antes cuando se reconectan y te dejan margen para crecer sin tener que cambiar el número de shards cada pocas semanas.
ShardingManager, procesos y clusters
discord.js ofrece tres formas de organizarlo:
| Opción | Cómo funciona | Cuándo encaja |
|---|---|---|
Sharding interno (shards: 'auto' en el cliente) |
Varios shards en un solo proceso | Bots pequeños; la guía lo desaconseja para bots grandes por el consumo de memoria |
ShardingManager |
Un proceso (o hilo) por shard en la misma máquina | De unos pocos miles a decenas de miles de servidores |
| Clusters | Grupos de shards por proceso, repartidos en varias máquinas o contenedores | Bots grandes que no caben en una máquina |
El arranque con ShardingManager en TypeScript es corto:
import { ShardingManager } from 'discord.js';
const manager = new ShardingManager('./dist/bot.js', {
token: process.env.DISCORD_TOKEN,
totalShards: 'auto',
});
manager.on('shardCreate', (shard) => {
console.log(`Shard ${shard.id} lanzado`);
});
await manager.spawn();
bot.js es tu bot normal, el de la guía para crear un bot de Discord. Cada proceso hijo recibe su identificador de shard y solo se conecta a lo que le toca.
Para datos agregados, client.shard permite preguntar a todos los shards:
const counts = await client.shard!.fetchClientValues('guilds.cache.size') as number[];
const totalGuilds = counts.reduce((acc, n) => acc + n, 0);
Funciona, pero cada llamada recorre todos los procesos. Con cientos de shards no lo pondría en un comando que cualquiera puede ejecutar. Esos datos los calcularía en segundo plano y los guardaría en una base de datos o en Redis.
Cuando un proceso por shard deja de ser razonable en memoria, el paso habitual es agrupar varios shards por proceso y repartir esos grupos entre máquinas. discord.js deja indicar qué shards abre cada cliente con las opciones shards y shardCount; hay librerías de la comunidad, como discord-hybrid-sharding, que gestionan esa coordinación. En cualquier caso, la coordinación entre máquinas pasa a ser responsabilidad tuya.
Caché: limita lo que guarda discord.js
Por defecto discord.js cachea mucho: mensajes, miembros, presencias, usuarios. En un bot pequeño no se nota; con cientos de miles de servidores es lo primero que se come la memoria.
Dos herramientas: limitar el tamaño de cada caché y barrer periódicamente lo antiguo.
import { Client, GatewayIntentBits, Options } from 'discord.js';
const client = new Client({
intents: [GatewayIntentBits.Guilds],
makeCache: Options.cacheWithLimits({
...Options.DefaultMakeCacheSettings,
MessageManager: 0,
PresenceManager: 0,
ReactionManager: 0,
GuildMemberManager: {
maxSize: 200,
keepOverLimit: (member) => member.id === member.client.user.id,
},
}),
sweepers: {
...Options.DefaultSweeperSettings,
users: {
interval: 3_600,
filter: () => (user) => user.id !== user.client.user.id,
},
},
});
La regla que sigo: si una función necesita un dato, lo pido a la API o a mi base de datos en ese momento. La caché de discord.js es un atajo, no una fuente de verdad.
Los intents también son caché. Cada intent que activas significa más eventos y más objetos en memoria. Pide solo los que uses.
Estado fuera del proceso: PostgreSQL y Redis
Con sharding, dos procesos pueden atender a un mismo usuario en servidores distintos. Si guardas algo en una variable en memoria, cada shard tiene su propia copia y ninguna es la buena.
Mi reparto habitual:
- PostgreSQL para lo que tiene que durar: configuración de cada servidor, datos de usuarios, suscripciones premium, registros de moderación.
- Redis para estado compartido y efímero: cooldowns, contadores, locks, caché de lecturas frecuentes de la base de datos.
Un cooldown compartido entre todos los shards cabe en pocas líneas:
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
export async function tryCooldown(userId: string, command: string, seconds: number) {
const key = `cooldown:${command}:${userId}`;
const result = await redis.set(key, '1', { NX: true, EX: seconds });
return result === 'OK';
}
Si devuelve false, el usuario sigue en cooldown, da igual en qué shard haya ejecutado el comando antes.
Colas para el trabajo pesado
Algunas tareas no deberían ejecutarse dentro del proceso que mantiene la conexión con el gateway. Un ejemplo claro son las imágenes de bienvenida personalizadas, una función que CactusFire ofrece: generar una imagen consume CPU y, si un servidor grande recibe muchos miembros de golpe, bloquea el event loop justo del shard que tiene que seguir respondiendo.
Lo que haría es separar productor y consumidor: el shard recibe el evento, mete un trabajo en una cola y sigue; uno o varios workers generan la imagen y la envían.
import { Queue } from 'bullmq';
const welcomeQueue = new Queue('welcome-cards', {
connection: { host: process.env.REDIS_HOST, port: 6379 },
});
await welcomeQueue.add(
'render',
{ guildId: '123', userId: '456', channelId: '789' },
{ attempts: 3, removeOnComplete: true },
);
Ventajas: los workers escalan aparte de los shards, puedes limitar cuántas imágenes se generan a la vez y un pico en un servidor no afecta al resto. El worker no necesita conexión al gateway; le basta la API REST para enviar el mensaje.
Rate limits: los que más se notan al escalar
- Identify: hay un límite diario de inicios de sesión (1.000 cada 24 horas según la documentación actual) y un número de shards que pueden identificarse a la vez cada 5 segundos (
max_concurrency). Si reinicias todos los shards en bucle por un error, puedes agotarlo. - Gateway: cada conexión puede enviar 120 eventos cada 60 segundos. Si te pasas, Discord corta la conexión.
- API REST: hay límites por ruta y un límite global.
@discordjs/restlos gestiona y encola las peticiones, pero no hace milagros: si tu bot envía mensajes masivos, diseña la función para que no lo haga. - Interacciones: tienes unos 3 segundos para responder o hacer
deferReply(). Bajo carga, cualquier cosa lenta en el handler acaba en “la aplicación no ha respondido”.
Observabilidad: saber qué shard falla
Con cientos de shards, “el bot va lento” no te dice nada. Lo mínimo que mediría por shard:
- Estado de la conexión y latencia (
client.ws.ping). - Eventos
shardDisconnect,shardReconnecting,shardResumeyshardErrordel cliente. - Memoria del proceso y número de servidores.
- Tiempo de respuesta de cada comando y tasa de errores.
Todo con el identificador del shard en cada log. Cuando un servidor se queja, calculas su shard con la fórmula de arriba y vas directo a sus métricas.
Despliegues sin cortar el servicio
Reiniciar todo de golpe significa que cientos de shards vuelven a identificarse a la vez, que el bot está desconectado unos minutos y que te arriesgas a chocar con el límite de identify.
Lo que me funciona como principio:
- Construir la imagen en CI (GitHub Actions o similar) y desplegar la misma imagen en todos los contenedores.
- Reiniciar por grupos de shards, esperando a que cada grupo esté listo antes de pasar al siguiente.
- Separar en otros servicios lo que cambia a menudo (paneles web, workers de colas), para no reiniciar shards por cada cambio.
- Registrar los comandos slash en un paso aparte del despliegue, solo cuando cambia su definición.
Qué aprendí escalando un bot de Discord
El sharding en sí es la parte fácil: discord.js te lo da casi hecho. Lo difícil viene con lo que el sharding rompe: cualquier dato que vivía en memoria, cualquier comando que asumía ver todos los servidores, cualquier tarea pesada que compartía proceso con la conexión al gateway. Si diseñas desde el principio con estado fuera del proceso y trabajo pesado en colas, pasar de 1 a 300 shards es sobre todo una cuestión de infraestructura.
Si te encuentras en ese punto con tu bot y quieres comentarlo, aquí me tienes.
Preguntas frecuentes
¿A partir de cuántos servidores necesita sharding un bot de Discord?
Discord obliga a usar sharding cuando el bot está en 2.500 servidores o más, y cada shard admite como máximo 2.500. La guía de discord.js recomienda prepararlo hacia los 2.000 servidores.
¿Cuántos servidores debería llevar cada shard?
La guía de discord.js recomienda alrededor de 1.000 servidores por shard. CactusFire, con más de 295.000 servidores y 304 shards, está en torno a 970 por shard.
¿Es lo mismo un shard que un proceso?
No. Un shard es una conexión al gateway. Puedes tener un shard por proceso, como hace ShardingManager por defecto, o agrupar varios shards en el mismo proceso para ahorrar memoria.
¿El sharding afecta a los comandos slash?
Si no configuras un endpoint HTTP para interacciones, llegan por el gateway al shard del servidor correspondiente, así que sí dependen del sharding. Las llamadas a la API REST, en cambio, no van ligadas a ningún shard.