Voice
player.voices holds one VoiceState per connected guild. A voice connection requires two Discord gateway events to establish — Discolink handles them once you forward their raw payloads.
Forwarding payloads
Section titled “Forwarding payloads”When your bot joins a voice channel, Discord sends VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE. Discolink waits for both, then makes a request to Lavalink to register the player. Forward all raw dispatches — Discolink filters for them internally:
// 'rawWS' for erisclient.on("raw", (payload) => player.voices.handleDispatch(payload));handleDispatch processes three event types:
VOICE_STATE_UPDATE— captures the bot’s session ID and channelVOICE_SERVER_UPDATE— captures the endpoint and token, then completes the connectionREADY— triggersplayer.init()automatically whenautoInit: true
Connecting
Section titled “Connecting”const voice = await player.voices.connect(guildId, voiceChannelId);
// With options (only applied when creating a new connection)await player.voices.connect(guildId, voiceChannelId, { node: "eu-west", // pin to a specific node volume: 80, // initial volume for the queue (0–1000) filters: { timescale: { speed: 1.1 } }, // initial filters context: { textChannelId }, // initial queue context});If a connection to the same guild and channel is already in progress, connect() returns the existing promise rather than starting a new one. If the bot is already connected to that channel and the connection is live, it returns the existing VoiceState immediately.
connect() times out after 30 seconds if the gateway events don’t arrive.
Why a connection might fail
Section titled “Why a connection might fail”forwardVoiceUpdatenot wired up — the bot never sends the voice state update to Discord, so the gateway never sends the required events back.handleDispatchnot wired up —VOICE_STATE_UPDATE/VOICE_SERVER_UPDATEare never forwarded to discolink, so it never receives the data it needs to complete the connection.- Missing
GuildVoiceStatesintent — Discord won’t sendVOICE_STATE_UPDATEwithout it, which means the session ID is never captured. - Missing channel permissions — the bot needs
Connect(andSpeakto produce audio) on the target channel. - Voice channel is full — easy to miss: when the channel has hit its user limit, Discord does not send any voice updates, so the connection times out. The bot needs the
Move Memberspermission to bypass the limit and receive both events.
Node selection for voice
Section titled “Node selection for voice”When a new connection is established, Discolink picks the best node for the voice region. It uses VoiceRegion.getRelevantNode(), which:
- Runs
NodeManager.relevant()to get ready nodes sorted by load metrics - Sorts that list by average voice WebSocket ping for the region (lower = better)
- Returns the first result
Ping data is collected from queueUpdate ticks — each tick carries the voice WebSocket latency from Lavalink. The average is computed over the current statsInterval window (default 60s). Nodes with no ping data for the region yet are treated as having 0ms ping, so they’re tried first until data accumulates.
Connection state
Section titled “Connection state”const voice = player.voices.get(guildId)!;
voice.connected; // true when states of both bot and Lavalink player report connectedvoice.reconnecting; // true during a voice reconnectvoice.disconnected; // true when not connected and not reconnectingvoice.changingNode; // true while changeNode() is in progressvoice.destroyed; // true if this instance is no longer in VoiceManager
voice.channelId; // current voice channel IDvoice.regionId; // voice region extracted from the endpoint (e.g. 'us-east')voice.ping; // voice WebSocket latency in ms, sourced from Lavalink player statevoice.node; // the Node this connection is on
voice.selfDeaf; // whether the bot has deafened itselfvoice.selfMute; // whether the bot has muted itselfvoice.serverDeaf; // whether the guild has deafened the botvoice.serverMute; // whether the guild has muted the botvoice.suppressed; // whether the bot is suppressed (stage channels)Automatic voice reconnection
Section titled “Automatic voice reconnection”When a voice connection is severed, Discolink emits voiceClose after having received the corresponding event from the connection’s node and checks the close code. For recoverable codes, it calls voice.connect() to reconnect. If reconnection fails, the voice connection and its queue are destroyed.
Disconnecting and destroying
Section titled “Disconnecting and destroying”// Leave the channel but keep the VoiceState and queue aliveawait player.voices.disconnect(guildId);
// Tear everything down — destroys the queue first, then the VoiceStateawait player.voices.destroy(guildId, "User left the channel");destroy() is the right call when you’re done with a guild — on channel delete, bot kick, or the last user leaving. If a queue exists for the guild, voices.destroy() delegates to queues.destroy() and vice versa.
You can also call these directly on a VoiceState instance:
const voice = player.voices.get(guildId)!;await voice.connect(); // connect to the same channelawait voice.connect(otherChannelId); // move to a different channelawait voice.disconnect(); // disconnect from the channelawait voice.destroy("reason"); // destroy with reasonMoving to a different node
Section titled “Moving to a different node”await player.voices.get(guildId)!.changeNode("eu-west");changeNode() migrates the player to the new node:
- Destroys the player on the old node
- Creates it on the new node with the same state
- Includes and resumes any active track if its source is supported
- Emits
voiceChangewith the previous node and whether playback was active
If changeNode() is already in progress for this voice state, the call returns the existing promise.
With relocateQueues: true (the default), this happens automatically when a node closes or disconnects. The relocation algorithm distributes queues across available nodes proportionally by their load scores.
Full API: VoiceState