Multi-Server Discord Bots: Architecture for Bots Running in 1000+ Servers
The biggest architectural mistake in Discord bot development: building a bot for your small test server and then deploying it to thousands of servers without redesigning for scale. Here is what changes as bot scale increases.
The Scale Inflection Points
< 100 servers: Simple architecture works. In-memory settings, single-process, SQLite.
100–2,499 servers: Memory pressure from caching. Migrate to persistent database. Audit cache configuration.
2,500+ servers: Sharding required (Discord mandate). Migrate to PostgreSQL. Implement per-guild async processing.
Database per Server vs. Global Database
Single global database (recommended):
CREATE TABLE guild_settings (
guild_id TEXT PRIMARY KEY,
prefix TEXT DEFAULT '!',
language TEXT DEFAULT 'en',
welcome_channel TEXT
);
All guilds share one database. Queries are guild-scoped. This is the correct approach.
Separate database per server — Do not do this. Operational complexity is immense and provides no meaningful isolation benefit.
Async Event Processing for Scale
At 100+ servers, events arrive simultaneously from multiple guilds. Synchronous per-event processing creates queue bottlenecks:
// Process events asynchronously with a queue
const Queue = require('bull');
const messageQueue = new Queue('message-processing', redisConfig);
client.on('messageCreate', (message) => {
// Queue the processing, don't await it
messageQueue.add({ messageId: message.id, guildId: message.guildId, content: message.content });
});
// Worker processes queue items
messageQueue.process(async (job) => {
const { messageId, guildId, content } = job.data;
await processMessage(messageId, guildId, content);
});
Redis-backed queues (Bull) allow processing events across multiple worker processes and provide retry logic for failures.
Caching Guild Settings
For 1000 guilds, a database read on every message event is 1000s of db queries per second. Cache settings in Redis:
const redis = require('redis').createClient();
async function getGuildSettings(guildId) {
// Check cache first
const cached = await redis.get(`guild:${guildId}:settings`);
if (cached) return JSON.parse(cached);
// Fetch from database
const settings = await db.query('SELECT * FROM guild_settings WHERE guild_id = $1', [guildId]);
// Cache for 5 minutes
await redis.setEx(`guild:${guildId}:settings`, 300, JSON.stringify(settings.rows[0]));
return settings.rows[0];
}
This pattern reduces database load by 95%+ for frequently-accessed settings.