As your Discord music bot grows, a single Lavalink server may not be enough. Load balancing across multiple nodes provides scalability, redundancy, and better performance. This guide covers everything you need to know.
Consider multiple Lavalink nodes when:
Distributes players evenly across nodes in order.
Pros:
Cons:
Sends new players to the node with fewest active players.
Pros:
Cons:
Assigns weights based on node capacity.
Pros:
Cons:
Routes players to nearest node.
Pros:
Cons:
Each Lavalink node needs unique identification:
Node 1 (US East):
server:
port: 2333
address: 0.0.0.0
lavalink:
server:
password: "shared_password"
Node 2 (Europe):
server:
port: 2333
address: 0.0.0.0
lavalink:
server:
password: "shared_password"
Configure your bot to connect to multiple nodes:
Shoukaku (Node.js):
const { Shoukaku, Connectors } = require('shoukaku');
const nodes = [
{
name: 'us-east',
url: 'lavalink-us.example.com:2333',
auth: 'shared_password'
},
{
name: 'europe',
url: 'lavalink-eu.example.com:2333',
auth: 'shared_password'
}
];
const shoukaku = new Shoukaku(
new Connectors.DiscordJS(client),
nodes,
{
moveOnDisconnect: true,
resumable: true,
resumableTimeout: 30
}
);
Erela.js:
const { Manager } = require('erela.js');
const manager = new Manager({
nodes: [
{
host: 'lavalink-us.example.com',
port: 2333,
password: 'shared_password',
identifier: 'us-east'
},
{
host: 'lavalink-eu.example.com',
port: 2333,
password: 'shared_password',
identifier: 'europe'
}
],
autoPlay: true
});
function getLeastLoadedNode(shoukaku) {
const nodes = [...shoukaku.nodes.values()];
return nodes
.filter(node => node.state === 'CONNECTED')
.sort((a, b) => a.players.size - b.players.size)[0];
}
// Usage
const node = getLeastLoadedNode(shoukaku);
const player = await node.joinChannel({
guildId: guildId,
channelId: channelId,
shardId: 0
});
const nodeWeights = {
'us-east': 3, // 3x capacity
'europe': 2, // 2x capacity
'asia': 1 // 1x capacity
};
function getWeightedNode(shoukaku) {
const nodes = [...shoukaku.nodes.values()]
.filter(node => node.state === 'CONNECTED');
let totalWeight = 0;
const weightedNodes = nodes.map(node => {
const weight = nodeWeights[node.name] || 1;
const adjustedWeight = weight / (node.players.size + 1);
totalWeight += adjustedWeight;
return { node, weight: adjustedWeight };
});
let random = Math.random() * totalWeight;
for (const { node, weight } of weightedNodes) {
random -= weight;
if (random <= 0) return node;
}
return weightedNodes[0].node;
}
const guildRegions = new Map();
function getRegionalNode(shoukaku, guildId) {
const region = guildRegions.get(guildId) || 'us-east';
const preferredNode = shoukaku.nodes.get(region);
if (preferredNode?.state === 'CONNECTED') {
return preferredNode;
}
// Fallback to any available node
return getLeastLoadedNode(shoukaku);
}
// Set region based on voice server
client.on('voiceStateUpdate', (oldState, newState) => {
if (newState.channel) {
const region = newState.guild.preferredLocale;
guildRegions.set(newState.guild.id, mapLocaleToNode(region));
}
});
shoukaku.on('nodeDisconnect', (name, reason) => {
console.log(\`Node \${name} disconnected: \${reason}\`);
// Move players to another node
const disconnectedNode = shoukaku.nodes.get(name);
if (!disconnectedNode) return;
for (const [guildId, player] of disconnectedNode.players) {
const newNode = getLeastLoadedNode(shoukaku);
if (newNode) {
player.move(newNode.name);
}
}
});
shoukaku.on('nodeReconnect', (name) => {
console.log(\`Node \${name} reconnected\`);
// Optionally rebalance players
});
shoukaku.on('nodeError', (name, error) => {
console.error(\`Node \${name} error:\`, error);
});
setInterval(() => {
for (const [name, node] of shoukaku.nodes) {
console.log(\`Node \${name}:\`, {
state: node.state,
players: node.players.size,
ping: node.stats?.ping || 'N/A'
});
}
}, 30000);
function collectNodeMetrics(shoukaku) {
const metrics = [];
for (const [name, node] of shoukaku.nodes) {
metrics.push({
name,
state: node.state,
players: node.players.size,
cpu: node.stats?.cpu?.systemLoad || 0,
memory: node.stats?.memory?.used || 0,
uptime: node.stats?.uptime || 0
});
}
return metrics;
}
version: '3.8'
services:
lavalink-us:
image: ghcr.io/lavalink-devs/lavalink:4
ports:
- "2333:2333"
environment:
- _JAVA_OPTIONS=-Xmx2G
volumes:
- ./application.yml:/opt/Lavalink/application.yml
lavalink-eu:
image: ghcr.io/lavalink-devs/lavalink:4
ports:
- "2334:2333"
environment:
- _JAVA_OPTIONS=-Xmx2G
volumes:
- ./application.yml:/opt/Lavalink/application.yml
| Scale | Nodes | RAM per Node | Total Capacity |
|---|---|---|---|
| Medium | 2 | 1GB | ~300 players |
| Large | 3 | 2GB | ~750 players |
| Enterprise | 5+ | 4GB | 2000+ players |
For voting-based decisions, odd numbers prevent ties.
Place nodes in different regions for:
Keep application.yml identical across nodes (except identifiers).
Set up alerting for each node individually.
Regularly test node failure scenarios.
| Nodes | Monthly Cost |
|---|---|
| 2 | ~₹400-600 |
| 3 | ~₹600-900 |
| 5 | ~₹1000-1500 |
Providers like VexaNode offer multi-node setups with:
Start with 2 for redundancy. Add more based on player count (roughly 1 node per 200-300 concurrent players).
Yes, use weighted load balancing to account for differences.
With proper failover, players automatically move to healthy nodes with brief interruption.
For redundancy, at least one node should be in a different region.
Load balancing Lavalink nodes enables your music bot to scale beyond single-server limits while providing redundancy. Start with two nodes for failover, then add more as your bot grows.
VexaNode offers managed Lavalink clusters with built-in load balancing, automatic failover, and unified monitoring for hassle-free scaling.
Join thousands of satisfied users and experience the VexaNode difference today.
View Plans