Multi-Node Setup

Scale your music bot across multiple Lavalink nodes for improved performance, load distribution, and high availability.

Basic Multi-Node Configuration

Simple Multi-Node Setup

const { Aqua } = require('aqualink');

const aqua = new Aqua(client, [
    {
      host: 'node1.example.com',
      port: 2333,
      auth: 'node1_password',
      ssl: false,
      name: 'node-1',
      regions: ['us-east'],
    },
    {
      host: 'node2.example.com',
      port: 2333,
      auth: 'node2_password', 
      ssl: false,
      name: 'node-2',
      regions: ['us-west'],
    },
    {
      host: 'node3.example.com',
      port: 2333,
      auth: 'node3_password',
      ssl: false
      name: 'node-3',
      regions: ['eu-central'],
    }
  ],
  {
    nodeResolver: 'leastLoad' // Options: 'leastLoad', 'leastRest', 'random' (default: 'leastLoad')
  }
);

Load Balancing Strategies

AquaLink provides several built-in load balancing strategies that can be set using the loadBalancer option in the Aqua constructor.
  • leastLoad (default): Selects the node with the lowest overall load, calculated based on CPU usage, memory, playing players, and REST API calls. This is generally the best option for most use cases.
  • leastRest: Selects the node with the fewest REST API calls.
  • random: Selects a random connected node.
const { Aqua } = require('aqualink');

const aqua = new Aqua(client, 
  [ /* add your nodes here. */], 
  {
    loadBalancer: 'leastLoad' 
  }
);

Advanced Node Configuration

You can add custom properties to your node configurations for use in advanced, custom logic (like a custom node selector). The library itself will ignore these extra properties.

Example with Custom Properties

const nodeConfigs = [
  {
    host: 'premium-node.example.com',
    port: 2333,
    ssl: 'secure_password',
    name: 'premium-1',
    regions: ['us-east-1'],  
    priority: 1,
  }
];

Node Selection Logic

Custom Node Selector Example

While AquaLink has a built-in node selector based on the loadBalancer option, you can implement your own logic to choose nodes for specific players.
class CustomNodeSelector {
  constructor(aqua) {
    this.aqua = aqua;
  }
  
  selectNode(guildId, options = {}) {
    const availableNodes = this.getAvailableNodes();
    
    if (availableNodes.length === 0) {
      throw new Error('No available nodes');
    }
    
    if (options.preferredRegion) {
      const regionalNodes = availableNodes.filter(node => 
        node.regions.includes(options.preferredRegion)
      );
      if (regionalNodes.length > 0) {
        return this.selectBestNode(regionalNodes);
      }
    }
    
    return this.selectBestNode(availableNodes);
  }
  
  getAvailableNodes() {
    return Array.from(this.aqua.nodeMap.values())
      .filter(node => node.connected && !this.isNodeOverloaded(node))
      .sort((a, b) => this.getNodeScore(a) - this.getNodeScore(b)); 
  }
  
  isNodeOverloaded(node) {
    const stats = node.stats;
    if (!stats) return true; 
    
    const cpuUsage = stats.cpu?.systemLoad || 0;
    const memoryUsage = (stats.memory?.used / stats.memory?.allocated) * 100 || 0;
    const playerCount = stats.playingPlayers || 0;
    
    return cpuUsage > 85 || memoryUsage > 80 || playerCount > 100;
  }
  
  getNodeScore(node) {
    const stats = node.stats;
    if (!stats) return Infinity; 
    
    const cpuScore = (stats.cpu?.systemLoad || 0) * 10;
    const memoryScore = ((stats.memory?.used / stats.memory?.allocated) * 100 || 0);
    const playerScore = (stats.playingPlayers || 0);
    
    return cpuScore + memoryScore + playerScore;
  }
  
  selectBestNode(nodes) {
    return nodes[0];
  }
}

const nodeSelector = new CustomNodeSelector(aqua);

Player Migration

Seamless Migration Example

This example shows how you could manually migrate a player from one node to another, preserving its state.
async function migratePlayer(player, targetNode = null) {
  if (!targetNode) {
    targetNode = nodeSelector.selectNode(player.guildId);
  }
  
  if (player.nodes.name === targetNode.name) {
    console.log('Player already on target node');
    return player;
  }
  
  const state = {
    guildId: player.guildId,
    voiceChannel: player.voiceChannel,
    textChannel: player.textChannel,
    queue: player.queue.slice(),
    current: player.current,
    position: player.position,
    volume: player.volume,
    filters: player.filters.filters,
    paused: player.paused,
    loop: player.loop
  };
  
  console.log(`Migrating player from ${player.nodes.name} to ${targetNode.name}`);
  
  await player.destroy();
  
  const newPlayer = aqua.createPlayer(targetNode, {
    guildId: state.guildId,
    voiceChannel: state.voiceChannel,
    textChannel: state.textChannel,
  });
  
  await newPlayer.connect();
  
  if (state.current) {
    newPlayer.queue.push(state.current);
  }
  if (state.queue.length > 0) {
    newPlayer.queue.push(...state.queue);
  }
  
  if (newPlayer.queue.length > 0) {
    await newPlayer.play();
    
    if (state.position > 0) {
      newPlayer.seek(state.position);
    }
    if (state.paused) {
      newPlayer.pause(true);
    }
  }
  
  if (state.volume !== 100) {
    await newPlayer.setVolume(state.volume);
  }
  
  if (Object.keys(state.filters).length > 0) {
    newPlayer.filters.filters = state.filters;
    await newPlayer.filters.updateFilters();
  }
  
  newPlayer.setLoop(state.loop);
  
  console.log('Player migration completed');
  return newPlayer;
}

Node Monitoring

Health Monitoring Example

Here is an example of a class that periodically checks the health of all connected nodes.
class NodeHealthMonitor {
  constructor(aqua) {
    this.aqua = aqua;
    this.healthData = new Map();
  }
  
  startMonitoring(interval = 30000) {
    this.monitorInterval = setInterval(() => {
      this.checkAllNodes();
    }, interval);
    
    console.log('Node health monitoring started');
  }
  
  stopMonitoring() {
    if (this.monitorInterval) {
      clearInterval(this.monitorInterval);
      console.log('Node health monitoring stopped');
    }
  }
  
  async checkAllNodes() {
    for (const [id, node] of this.aqua.nodeMap) {
      await this.checkNodeHealth(node);
    }
  }
  
  async checkNodeHealth(node) {
    try {
      const stats = node.stats;
      const health = {
        nodeId: node.name,
        connected: node.connected,
        cpu: stats?.cpu?.systemLoad || 0,
        memory: stats?.memory ? (stats.memory.used / stats.memory.allocated) * 100 : 0,
        players: stats?.playingPlayers || 0,
        uptime: stats?.uptime || 0,
        timestamp: Date.now()
      };
      
      this.healthData.set(node.name, health);
    } catch (error) {
      console.error(`Health check failed for node ${node.name}:`, error);
    }
  }

  getHealthReport() {
    const report = {};
    for (const [nodeId, health] of this.healthData) {
      report[nodeId] = {
        status: health.connected ? 'online' : 'offline',
        cpu: `${health.cpu.toFixed(1)}%`,
        memory: `${health.memory.toFixed(1)}%`,
        players: health.players,
        uptime: this.formatUptime(health.uptime)
      };
    }
    return report;
  }
  
  formatUptime(uptime) {
    const days = Math.floor(uptime / (1000 * 60 * 60 * 24));
    const hours = Math.floor((uptime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
    const minutes = Math.floor((uptime % (1000 * 60 * 60)) / (1000 * 60));
    
    return `${days}d ${hours}h ${minutes}m`;
  }
}

const healthMonitor = new NodeHealthMonitor(aqua);
healthMonitor.startMonitoring();