Skip to main content
When the Gateway closes a WebSocket connection, it sends a close frame with a numeric code and human-readable reason. Understanding these codes helps you implement proper reconnection logic.

Close Code Reference

All Gateway close codes are defined in fluxer_gateway/src/utils/constants.erl:76-89.
CodeNameResumableDescription
4000Unknown ErrorYesAn unknown error occurred
4001Unknown OpcodeYesInvalid opcode sent
4002Decode ErrorYesInvalid JSON or payload
4003Not AuthenticatedYesSent restricted opcode before Identify
4004Authentication FailedNoInvalid token
4005Already AuthenticatedYesSent Identify after already authenticating
4007Invalid SequenceNoResume sequence number invalid
4008Rate LimitedNoExceeded rate limits
4009Session TimeoutYesHeartbeat timeout exceeded
4010Invalid ShardNoInvalid shard configuration
4011Sharding RequiredNoSession requires sharding
4012Invalid API VersionNoInvalid Gateway version
4013Acknowledgement BackpressureNoEvent buffer overflow
Close codes 4004, 4007, 4008, 4010, 4011, 4012, and 4013 are NOT resumable. You must create a new session with Identify.

Resumable Close Codes

These errors allow you to reconnect and resume your session.

4000: Unknown Error

Cause: An unexpected internal error occurred. Recovery:
  1. Reconnect to the Gateway
  2. Attempt to Resume with your session ID and last sequence
  3. If Resume fails, send a new Identify
Example:
WebSocket Close Frame:
{
  "code": 4000,
  "reason": "Unknown error"
}

4001: Unknown Opcode

Cause: You sent an opcode that doesn’t exist or isn’t supported. Recovery:
  1. Fix your client to not send invalid opcodes
  2. Reconnect and Resume
Common Mistakes:
  • Sending opcode 13 (skipped in spec)
  • Sending opcodes > 14
  • Typos in opcode field
Implementation: gateway_handler.erl:handle_gateway_payload/3 (unknown opcode case)

4002: Decode Error

Cause: The Gateway couldn’t parse your message. Common Reasons:
  • Invalid JSON syntax
  • Payload exceeds 4,096 bytes
  • Missing required fields (op)
  • Compression failure
  • Encoding mismatch
Recovery:
  1. Fix the payload format
  2. Ensure payloads are under 4,096 bytes
  3. Reconnect and Resume
Example Error:
WebSocket Close Frame:
{
  "code": 4002,
  "reason": "Payload too large"
}
Implementation: gateway_handler.erl:handle_incoming_data/2

4003: Not Authenticated

Cause: You sent an opcode that requires authentication before sending Identify. Restricted Opcodes:
  • Opcode 3: Presence Update
  • Opcode 4: Voice State Update
  • Opcode 8: Request Guild Members
  • Opcode 14: Lazy Request
Recovery:
  1. Reconnect
  2. Send Identify or Resume
  3. Wait for Ready/RESUMED
  4. Then send other opcodes
Example:
WebSocket Close Frame:
{
  "code": 4003,
  "reason": "Not authenticated"
}
Implementation: gateway_handler.erl:handle_gateway_payload/3

4005: Already Authenticated

Cause: You sent Identify after already authenticating. How This Happens:
  • Sent Identify twice
  • Sent Identify after Resume succeeded
Recovery:
  1. Reconnect
  2. Send only ONE Identify or Resume per connection
Example:
WebSocket Close Frame:
{
  "code": 4005,
  "reason": "Already authenticated"
}
Implementation: gateway_handler.erl:handle_gateway_payload/3 (identify when session_pid exists)

4009: Session Timeout

Cause: You didn’t respond to heartbeats within 45 seconds. Heartbeat Flow:
  1. Gateway sends Heartbeat (op 1) every ~13.75s
  2. Client responds with Heartbeat ACK
  3. Gateway waits up to 45s for ACK
  4. If no ACK, closes with 4009
Recovery:
  1. Ensure heartbeat timer is running
  2. Respond to ALL heartbeat requests
  3. Reconnect and Resume
Prevention:
// Pseudocode
setInterval(() => {
  if (Date.now() - lastHeartbeatAck > 45000) {
    // Connection is dead, reconnect
    reconnect();
  }
}, 5000);
Implementation: gateway_handler.erl:handle_heartbeat_state/6

Non-Resumable Close Codes

These errors require starting a fresh session with Identify.

4004: Authentication Failed

Cause: The token you provided is invalid. Common Reasons:
  • Token is malformed
  • Token has expired
  • Token was revoked
  • Wrong token type (bot vs user)
Recovery:
  1. Verify your token is correct
  2. Obtain a new token if expired
  3. Reconnect and send Identify with valid token
  4. Do NOT attempt Resume
Example:
WebSocket Close Frame:
{
  "code": 4004,
  "reason": "Invalid token"
}
This close code is NOT resumable. Do not attempt to Resume.
Implementation: gateway_handler.erl:start_session/3 (invalid_token case)

4007: Invalid Sequence

Cause: The sequence number in your Resume request is invalid. Common Reasons:
  • Sequence is too old (session expired)
  • Sequence is in the future (shouldn’t happen)
  • Session data was lost server-side
Recovery:
  1. Do NOT attempt Resume again
  2. Reconnect and send a new Identify
  3. Store sequence numbers more reliably
Example:
WebSocket Close Frame:
{
  "code": 4007,
  "reason": "Invalid sequence"
}
This close code is NOT resumable. Start a fresh session.
Implementation: gateway_handler.erl:handle_resume_invalid_seq/1

4008: Rate Limited

Cause: You exceeded Gateway rate limits. Rate Limits:
  • General: 120 events per 60 seconds
  • Request Guild Members: 3 per 10 seconds
  • Voice State Update: 10 per second (queued beyond)
Recovery:
  1. Wait at least 60 seconds before reconnecting
  2. Review your code to reduce event frequency
  3. Reconnect and send a new Identify
  4. Do NOT attempt Resume
Prevention:
// Implement client-side rate limiting
const rateLimiter = {
  events: [],
  canSend() {
    const now = Date.now();
    this.events = this.events.filter(t => now - t < 60000);
    if (this.events.length >= 120) return false;
    this.events.push(now);
    return true;
  }
};
Example:
WebSocket Close Frame:
{
  "code": 4008,
  "reason": "Rate limited"
}
This close code is NOT resumable. Wait before reconnecting.
Implementation: gateway_handler.erl:check_rate_limit/2

4012: Invalid API Version

Cause: You didn’t specify v=1 in the connection URL. Valid URL:
wss://gateway.fluxer.example/?v=1&encoding=json
Invalid URLs:
wss://gateway.fluxer.example/?encoding=json  # Missing v=1
wss://gateway.fluxer.example/?v=2            # Wrong version
Recovery:
  1. Update your connection URL to include v=1
  2. Reconnect
  3. Send Identify
Example:
WebSocket Close Frame:
{
  "code": 4012,
  "reason": "Invalid API version"
}
This close code is NOT resumable. Fix your URL.
Implementation: gateway_handler.erl:websocket_init/1 (version check)

4013: Acknowledgement Backpressure

Cause: Your client isn’t processing events fast enough, causing buffer overflow. Buffer Limits:
  • Event ACK buffer: 4,096 events
  • Reaction buffer: 512 events
  • Pending presence buffer: 2,048 events
Common Reasons:
  • Client is blocking/hanging
  • Network congestion
  • Not acknowledging events
Close Reason Format:
Acknowledgement backlog exceeded: kind=event_ack_buffer unacked=4096 current=4096 limit=4096 seq=5000 ack_seq=904
Recovery:
  1. Fix your client to process events faster
  2. Ensure event handlers are non-blocking
  3. Reconnect and send Identify (cannot Resume)
Prevention:
// Process events asynchronously
gateway.on('dispatch', async (event) => {
  // Don't block the event loop
  setImmediate(() => handleEvent(event));
});
Example:
WebSocket Close Frame:
{
  "code": 4013,
  "reason": "Acknowledgement backlog exceeded: kind=event_ack_buffer unacked=4096 current=4096 limit=4096 seq=5000 ack_seq=904"
}
This close code is NOT resumable. Optimize your client.
Implementation: session_dispatch.erl:report_buffer_overflow/4

Reconnection Strategy

Implement this flowchart for robust reconnection:

Implementation Example

class GatewayClient {
  handleClose(code, reason) {
    console.log(`Closed: ${code} - ${reason}`);
    
    const nonResumable = [4004, 4007, 4008, 4010, 4011, 4012, 4013];
    
    if (code === 4008) {
      // Rate limited
      setTimeout(() => this.connect(), 60000);
      this.sessionId = null; // Clear session
    } else if (nonResumable.includes(code)) {
      // Not resumable
      setTimeout(() => this.connect(), 1000 + Math.random() * 4000);
      this.sessionId = null;
    } else {
      // Resumable
      setTimeout(() => this.connect(), 1000);
      // Keep sessionId for Resume
    }
  }
  
  async connect() {
    this.ws = new WebSocket('wss://gateway.fluxer.example/?v=1&encoding=json');
    
    this.ws.on('open', () => {
      // Wait for Hello before sending Identify/Resume
    });
    
    this.ws.on('message', (data) => {
      const msg = JSON.parse(data);
      
      if (msg.op === 10) { // Hello
        if (this.sessionId) {
          this.resume();
        } else {
          this.identify();
        }
      }
    });
    
    this.ws.on('close', (code, reason) => {
      this.handleClose(code, reason.toString());
    });
  }
}

Monitoring

Track these metrics to detect issues:

Close Code Distribution

Monitor which close codes occur most frequently

Resume Success Rate

Track Resume success vs failure rate

Reconnection Time

Measure time to reconnect and resume

Heartbeat Latency

Monitor round-trip time for heartbeats

Summary

  • Reconnect immediately (or with short delay)
  • Attempt Resume with session ID and sequence
  • Fall back to Identify if Resume fails
  • Clear session ID
  • Reconnect with delay
  • Send fresh Identify
  • Fix underlying issue
  • Wait at least 60 seconds
  • Clear session ID
  • Reduce event frequency
  • Send Identify after delay
  • Optimize client event processing
  • Clear session ID
  • Fix blocking code
  • Send Identify after fixes

Build docs developers (and LLMs) love