How to fix REALTIME_DISCONNECT: Websocket disconnected from Realtime server in Supabase

SupabaseINTERMEDIATEMEDIUMUpdated Dec 29, 2025· 211 views

The "REALTIME_DISCONNECT: Websocket disconnected from Realtime server" error occurs when a WebSocket connection to Supabase Realtime unexpectedly closes. This can happen due to network interruptions, server-side timeouts, authentication token expiration, or quota limits. Implementing proper reconnection logic with exponential backoff is essential for maintaining reliable realtime features.

Quick fix

Fixes REALTIME_DISCONNECT: Websocket disconnected from Realtime server

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(supabaseUrl, supabaseKey)

const channel = supabase.channel('my-channel')

channel
  .on('postgres_changes', {
    event: '*',
    schema: 'public',
    table: 'messages'
  }, (payload) => {
    console.log('Change received:', payload)
  })
  .subscribe((status) => {
    console.log('Subscription status:', status)

    switch(status) {
      case 'SUBSCRIBED':
        console.log('✓ Successfully subscribed to channel')
        // Resume any paused operations
        break

      case 'CHANNEL_ERROR':
        console.error('✗ Channel error - attempting to recover')
        // Connection failed or dropped, will auto-reconnect
        break

      case 'TIMED_OUT':
        console.error('✗ Connection timed out - will retry')
        // Connection timeout, auto-retry will happen
        break

      case 'CLOSED':
        console.log('Channel closed - unsubscribing')
        // User intentionally unsubscribed
        break

      default:
        console.log('Unknown status:', status)
    }
  })
  1. Implement proper subscription status handling
  2. Implement exponential backoff reconnection logic
  3. Check network connectivity and firewall settings
  4. Refresh authentication tokens before expiration
supabase — error
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(supabaseUrl, supabaseKey)
const channel = supabase.channel('my-channel')
channel
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'messages'
}, (payload) => {
console.log('Change received:', payload)
})
.subscribe((status) => {
console.log('Subscription status:', status)
switch(status) {
case 'SUBSCRIBED':
console.log('✓ Successfully subscribed to channel')
// Resume any paused operations
break
case 'CHANNEL_ERROR':
console.error('✗ Channel error - attempting to recover')
// Connection failed or dropped, will auto-reconnect
break
case 'TIMED_OUT':
console.error('✗ Connection timed out - will retry')
// Connection timeout, auto-retry will happen
break
case 'CLOSED':
console.log('Channel closed - unsubscribing')
// User intentionally unsubscribed
break
default:
console.log('Unknown status:', status)
}
})
REALTIME_DISCONNECT: Websocket disconnected from Realtime server

What this error means

The "REALTIME_DISCONNECT: Websocket disconnected from Realtime server" error indicates that the WebSocket connection between your application and the Supabase Realtime server has been terminated. Unlike the REALTIME_TIMEOUT error which occurs during initial connection, REALTIME_DISCONNECT happens after a previously established connection drops. The Supabase Realtime service maintains heartbeat messages every 30 seconds to detect connection issues and automatically attempt reconnection with exponential backoff (1s, 2s, 5s, 10s). When a disconnect occurs, it's typically due to: 1. **Network failures** - Client loses internet connectivity or network changes (WiFi switching) 2. **Server-side timeouts** - Supabase server disconnects idle connections after a period 3. **Authentication expiration** - JWT token expires during a long-lived connection 4. **Rate limiting** - Project exceeds realtime quotas (too many messages, connections, or channel joins) 5. **Server maintenance** - Supabase performing updates or maintenance on realtime infrastructure 6. **Firewall/proxy issues** - Intermediary infrastructure terminating idle WebSocket connections

Typical symptoms

  • WebSocket connection drops unexpectedly after initial connection success
  • REALTIME_DISCONNECT error appears in browser console or application logs
  • Realtime features (live updates, presence, broadcast) stop working mid-session
  • Connection works initially but disconnects after a period of inactivity
  • Frequent disconnections on mobile or when switching networks
  • Subscription status changes from SUBSCRIBED to CHANNEL_ERROR or CLOSED
  • User experiences interrupted live updates or missing data changes
  • Application logs show repeated disconnect/reconnect cycles

Common causes

  1. Client network loss - WiFi dropout, mobile switching networks, device sleep/lock
  2. Corporate firewall/proxy terminating idle WebSocket connections
  3. Supabase server idle timeout - connections inactive for extended period
  4. Authentication token expiration - JWT expires during long connection
  5. Project quota exceeded - too many concurrent connections or messages per second
  6. Supabase service maintenance or node failures
  7. DNS resolution issues causing connection instability
  8. SSL/TLS renegotiation failures or certificate issues
  9. Memory pressure on Supabase Realtime servers
  10. Browser closing idle connections (some browsers have 5-minute defaults)
  11. Application bugs not properly handling subscription errors
  12. Incorrect error handling causing cascade failures

How to fix "REALTIME_DISCONNECT: Websocket disconnected from Realtime server"

1Implement proper subscription status handling

Add comprehensive handling for all subscription status changes:

javascript
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(supabaseUrl, supabaseKey)

const channel = supabase.channel('my-channel')

channel
  .on('postgres_changes', {
    event: '*',
    schema: 'public',
    table: 'messages'
  }, (payload) => {
    console.log('Change received:', payload)
  })
  .subscribe((status) => {
    console.log('Subscription status:', status)

    switch(status) {
      case 'SUBSCRIBED':
        console.log('✓ Successfully subscribed to channel')
        // Resume any paused operations
        break

      case 'CHANNEL_ERROR':
        console.error('✗ Channel error - attempting to recover')
        // Connection failed or dropped, will auto-reconnect
        break

      case 'TIMED_OUT':
        console.error('✗ Connection timed out - will retry')
        // Connection timeout, auto-retry will happen
        break

      case 'CLOSED':
        console.log('Channel closed - unsubscribing')
        // User intentionally unsubscribed
        break

      default:
        console.log('Unknown status:', status)
    }
  })

Key status codes to handle:
- SUBSCRIBED (0): Connection healthy, ready to receive updates
- CHANNEL_ERROR (1): Error occurred, automatic reconnection in progress
- TIMED_OUT (2): Connection timeout, retry with exponential backoff
- CLOSED (3): Channel intentionally closed

2Implement exponential backoff reconnection logic

Build a robust reconnection manager:

javascript
class RealtimeConnectionManager {
  constructor(supabase, options = {}) {
    this.supabase = supabase
    this.maxRetries = options.maxRetries || 10
    this.baseDelay = options.baseDelay || 1000 // 1 second
    this.maxDelay = options.maxDelay || 30000 // 30 seconds

    this.retryCount = 0
    this.channel = null
    this.lastSuccessfulConnection = null
  }

  async connect(channelName) {
    try {
      this.channel = this.supabase.channel(channelName)

      this.channel
        .on('postgres_changes', {
          event: '*',
          schema: 'public'
        }, (payload) => {
          console.log('Received change:', payload)
          this.lastSuccessfulConnection = Date.now()
        })
        .subscribe((status) => {
          if (status === 'SUBSCRIBED') {
            console.log('Connected successfully')
            this.retryCount = 0 // Reset on success
            this.lastSuccessfulConnection = Date.now()
          } else if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
            this.handleDisconnect()
          }
        })
    } catch (error) {
      this.handleDisconnect(error)
    }
  }

  handleDisconnect(error) {
    console.error('Realtime disconnected:', error)

    if (this.retryCount < this.maxRetries) {
      // Calculate exponential backoff with jitter
      const exponentialDelay = Math.pow(2, this.retryCount) * this.baseDelay
      const jitter = Math.random() * 1000 // Add up to 1 second random jitter
      const delay = Math.min(exponentialDelay + jitter, this.maxDelay)

      this.retryCount++

      console.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${this.retryCount}/${this.maxRetries})`)

      setTimeout(() => {
        this.connect(this.channel?.name || 'default')
      }, delay)
    } else {
      console.error('Max reconnection attempts reached. Manual intervention needed.')
      // Emit event to notify application
      this.emit('max-retries-reached')
    }
  }

  disconnect() {
    this.retryCount = 0
    if (this.channel) {
      this.supabase.removeChannel(this.channel)
    }
  }

  emit(event) {
    // Implement your own event emitter or use callbacks
    console.log('Event:', event)
  }
}

// Usage
const manager = new RealtimeConnectionManager(supabase, {
  maxRetries: 10,
  baseDelay: 1000,
  maxDelay: 30000
})

await manager.connect('my-channel')
3Check network connectivity and firewall settings

Verify network configuration to prevent unexpected disconnections:

bash
# Test WebSocket connectivity
npm install -g wscat
wscat -c "wss://YOUR_PROJECT_REF.supabase.co/realtime/v1"

# Verify DNS resolution
nslookup YOUR_PROJECT_REF.supabase.co

# Test with curl
curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
  -H "Sec-WebSocket-Version: 13" \
  https://YOUR_PROJECT_REF.supabase.co/realtime/v1

Configuration steps:
1. Whitelist Supabase domain in corporate firewalls:
- Add *.supabase.co to allowed domains
- Allow port 443 (wss uses port 443 like HTTPS)

2. Disable firewall idle timeout for WebSocket connections:
- Configure proxy to not timeout idle WebSocket connections
- Some proxies close connections after 5-15 minutes of inactivity

3. Test from different networks:
- Try from home/mobile network vs corporate network
- Test with VPN enabled/disabled

4. Check system clock:
- Incorrect time can cause SSL/TLS issues
- Verify system time is synchronized: date

4Refresh authentication tokens before expiration

Prevent disconnections due to token expiration:

javascript
// Implement token refresh before expiration
async function setupTokenRefresh(supabase) {
  const { data: { session } } = await supabase.auth.getSession()

  if (session?.expires_at) {
    // Calculate when token expires
    const expiresAt = session.expires_at * 1000 // Convert to milliseconds
    const now = Date.now()
    const timeUntilExpiry = expiresAt - now

    // Refresh token 5 minutes before expiration
    const refreshTime = timeUntilExpiry - (5 * 60 * 1000)

    setTimeout(async () => {
      console.log('Refreshing authentication token...')
      const { data, error } = await supabase.auth.refreshSession()

      if (error) {
        console.error('Token refresh failed:', error)
      } else {
        console.log('Token refreshed successfully')
        // Restart token refresh cycle
        setupTokenRefresh(supabase)
      }
    }, Math.max(refreshTime, 0))
  }
}

// Setup on app initialization
setupTokenRefresh(supabase)

// Also refresh on auth state change
supabase.auth.onAuthStateChange(async (event, session) => {
  if (event === 'TOKEN_REFRESHED') {
    console.log('Token automatically refreshed')
    setupTokenRefresh(supabase)
  }
})

Additional authentication strategies:
1. Use session persistence to restore auth across page reloads
2. Listen to session changes and reconnect realtime when auth updates
3. Handle silent sign-out and reconnect after fresh authentication
4. Validate token on server-side for sensitive operations

5Monitor and respect Realtime quotas

Avoid quota-related disconnections:

javascript
// Check current plan limitations
const planLimits = {
  free: {
    maxConcurrentConnections: 2,
    maxChannelsPerConnection: 100,
    maxEventsPerSecond: 100,
    maxChannelJoinsPerSecond: 5
  },
  pro: {
    maxConcurrentConnections: 'unlimited',
    maxChannelsPerConnection: 500,
    maxEventsPerSecond: 'unlimited',
    maxChannelJoinsPerSecond: 50
  }
}

// Implement message rate limiting
class MessageRateLimiter {
  constructor(maxMessagesPerSecond = 10) {
    this.maxMessagesPerSecond = maxMessagesPerSecond
    this.messageTimestamps = []
  }

  canSend() {
    const now = Date.now()
    // Remove old timestamps outside 1-second window
    this.messageTimestamps = this.messageTimestamps.filter(
      ts => now - ts < 1000
    )

    if (this.messageTimestamps.length < this.maxMessagesPerSecond) {
      this.messageTimestamps.push(now)
      return true
    }
    return false
  }

  async sendSafe(channel, event, payload) {
    if (!this.canSend()) {
      console.warn('Rate limit exceeded, queuing message...')
      // Implement queue or back off
      return false
    }

    channel.send({
      type: 'broadcast',
      event: event,
      payload: payload
    })
    return true
  }
}

// Monitor connection health
async function monitorConnectionHealth(channel) {
  let lastMessageTime = Date.now()

  channel.on('heartbeat', (payload) => {
    lastMessageTime = Date.now()
  })

  // Check if connection is stale
  setInterval(() => {
    const timeSinceLastMessage = Date.now() - lastMessageTime
    const maxStaleTime = 60000 // 60 seconds

    if (timeSinceLastMessage > maxStaleTime) {
      console.warn('No messages for', timeSinceLastMessage, 'ms - connection may be stale')
      // Trigger reconnection
    }
  }, 30000) // Check every 30 seconds
}

Quota management tips:
1. Share channels instead of creating separate ones per user
2. Use channel presence instead of broadcast for real-time sync
3. Batch messages instead of sending individual updates
4. Upgrade plan if consistently hitting Free tier limits

6Add persistence and offline support

Handle disconnections gracefully with data persistence:

javascript
class PersistentRealtimeClient {
  constructor(supabase, options = {}) {
    this.supabase = supabase
    this.isOnline = navigator.onLine
    this.pendingChanges = []
    this.cachedData = new Map()

    // Listen for network changes
    window.addEventListener('online', () => this.handleOnline())
    window.addEventListener('offline', () => this.handleOffline())
  }

  handleOffline() {
    console.log('Application offline - queuing changes')
    this.isOnline = false
    // Stop attempting reconnections
  }

  async handleOnline() {
    console.log('Application online - syncing changes')
    this.isOnline = true
    // Process pending changes
    await this.processPendingChanges()
    // Reconnect realtime
  }

  async processPendingChanges() {
    for (const change of this.pendingChanges) {
      try {
        await this.applyChange(change)
        this.pendingChanges = this.pendingChanges.filter(c => c !== change)
      } catch (error) {
        console.error('Failed to apply change:', error)
      }
    }
  }

  async applyChange(change) {
    // Send change to server
    const { error } = await this.supabase
      .from('table_name')
      .update(change)
      .eq('id', change.id)

    if (error) throw error
  }

  cacheMessage(key, data) {
    this.cachedData.set(key, {
      data,
      timestamp: Date.now()
    })
  }

  getCached(key) {
    return this.cachedData.get(key)?.data
  }

  subscribe(channel) {
    return channel.subscribe((status) => {
      if (status === 'SUBSCRIBED' && this.pendingChanges.length > 0) {
        // Sync pending changes
        this.processPendingChanges()
      }
    })
  }
}

// Usage with React
import { useEffect, useState } from 'react'

function useRealtimeWithPersistence(channel) {
  const [data, setData] = useState([])
  const [isConnected, setIsConnected] = useState(false)

  useEffect(() => {
    const client = new PersistentRealtimeClient(supabase)

    const subscription = client.subscribe(channel)
      .on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, (payload) => {
        setData(prev => [...prev, payload.new])
        client.cacheMessage('messages', [...data, payload.new])
      })
      .subscribe((status) => {
        setIsConnected(status === 'SUBSCRIBED')
      })

    // Load cached data on mount
    const cached = client.getCached('messages')
    if (cached) {
      setData(cached)
    }

    return () => {
      supabase.removeChannel(subscription)
    }
  }, [])

  return { data, isConnected }
}
7Debug with logging and monitoring

Enable comprehensive logging to diagnose disconnect issues:

javascript
// Create a logging wrapper for Realtime debugging
class RealtimeDebugger {
  constructor(supabase) {
    this.supabase = supabase
    this.eventLog = []
    this.connectionLog = []
  }

  subscribeWithLogging(channel) {
    return channel
      .subscribe((status) => {
        const logEntry = {
          timestamp: new Date().toISOString(),
          status,
          sessionStorage: {
            isOnline: navigator.onLine,
            timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
          }
        }

        this.connectionLog.push(logEntry)
        console.log('[Realtime Debug]', logEntry)

        // Upload logs periodically for analysis
        if (this.connectionLog.length % 10 === 0) {
          this.uploadLogs()
        }
      })
  }

  logEvent(eventType, payload) {
    const logEntry = {
      timestamp: Date.now(),
      eventType,
      payload,
      browserInfo: {
        userAgent: navigator.userAgent,
        connection: navigator.connection?.effectiveType,
        onLine: navigator.onLine
      }
    }

    this.eventLog.push(logEntry)
    console.log('[Event Log]', logEntry)
  }

  async uploadLogs() {
    try {
      // Send logs to your backend for analysis
      await fetch('/api/realtime-logs', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          connectionLog: this.connectionLog.slice(-50), // Last 50 entries
          eventLog: this.eventLog.slice(-100) // Last 100 entries
        })
      })
    } catch (error) {
      console.error('Failed to upload logs:', error)
    }
  }

  getConnectionStats() {
    const disconnects = this.connectionLog.filter(e =>
      e.status === 'CHANNEL_ERROR' || e.status === 'TIMED_OUT'
    ).length

    const connects = this.connectionLog.filter(e =>
      e.status === 'SUBSCRIBED'
    ).length

    return {
      totalConnections: connects,
      totalDisconnects: disconnects,
      ratio: connects > 0 ? (disconnects / connects).toFixed(2) : 0
    }
  }
}

// Enable debug logging in Supabase client
const supabase = createClient(supabaseUrl, supabaseKey, {
  realtime: {
    logLevel: 'debug'
  }
})

const debugger = new RealtimeDebugger(supabase)
const channel = supabase.channel('debug-channel')
debugger.subscribeWithLogging(channel)

// Periodically log stats
setInterval(() => {
  console.log('Realtime Stats:', debugger.getConnectionStats())
}, 60000) // Every minute

Check Supabase Status and Community:
1. Visit [status.supabase.com](https://status.supabase.com) for platform status
2. Check [github.com/supabase/realtime](https://github.com/supabase/realtime) for known issues
3. Search Discord or discussions for similar reports
4. Contact support with: connection logs, reproduction steps, affected users count

Advanced notes

## Supabase Realtime Disconnect Deep Dive

### Automatic Reconnection Behavior

The @supabase/realtime-js client implements automatic reconnection:
- Heartbeat: Sent every 30 seconds to keep connection alive
- Backoff strategy: 1s → 2s → 5s → 10s exponential backoff
- Max retries: Continues indefinitely after initial connection established
- Fail-fast on initial: Throws error if cannot connect initially

### Mobile App Considerations

Mobile applications have special challenges:
1. Network switching: Switching from WiFi to cellular creates brief disconnect
2. App backgrounding: iOS/Android may close connections when app backgrounded
3. Lock screen: Some devices terminate idle connections when locked
4. Battery optimization: Low power mode may affect connection stability

Solutions:
- Implement heartbeat/keep-alive on client
- Resume subscriptions when app returns to foreground
- Use local notification when realtime reconnects
- Avoid restarting connection immediately after backgrounding

### Browser-Specific Issues

1. Shared Workers: Multiple tabs create separate WebSocket connections
- Solution: Use Shared Workers to multiplex connections
2. Service Workers: Can interfere with WebSocket reconnection
3. Incognito Mode: Some restrictions on WebSocket duration
4. Plugin interference: Security/privacy plugins blocking WebSockets

### Quota-Related Disconnects

Supabase disconnects if quotas exceeded:
- Free tier: 2 concurrent connections per project
- Message throughput: Disconnect if exceeds rate limits
- Channel joins: Limited joins per second
- Presence updates: Limited presence message frequency

Monitor with: SELECT COUNT(*) FROM pg_stat_activity WHERE application_name LIKE '%realtime%'

### Network Proxy Issues

Some corporate environments have problematic proxy setups:
1. SSL Inspection: Breaks WebSocket TLS negotiation
2. Idle timeout: Proxy closes idle connections after 5-15 minutes
3. Compression: Proxy tries to compress WebSocket frames (breaks protocol)
4. Rate limiting: Proxy limits connection frequency

Workarounds:
- Add proxy certificate to system trust store
- Configure proxy timeout settings higher
- Disable proxy for Supabase domains if possible
- Use VPN as alternative

### Performance Metrics to Monitor

Track these to understand reconnection patterns:
1. Connection success rate: (Total successful connects) / (Total attempts)
2. Time to reconnect: Average time from disconnect to SUBSCRIBED
3. Disconnect frequency: Disconnects per user per day
4. Message latency: Time from database change to client receipt
5. Error rate: Failed operations / total operations

### Fallback Architecture

For critical applications, implement multi-layer fallback:

javascript
// Tier 1: Realtime (immediate updates)
const realtimeChannel = supabase.channel('tier1')

// Tier 2: Polling (every 5 seconds if realtime down)
let pollInterval
realtimeChannel.subscribe((status) => {
  if (status !== 'SUBSCRIBED' && !pollInterval) {
    console.log('Realtime down, starting poll')
    pollInterval = setInterval(async () => {
      const { data } = await supabase.from('table').select()
      // Process data
    }, 5000)
  } else if (status === 'SUBSCRIBED' && pollInterval) {
    console.log('Realtime restored, stopping poll')
    clearInterval(pollInterval)
    pollInterval = null
  }
})

// Tier 3: Manual refresh (user triggers)
button.addEventListener('click', async () => {
  const { data } = await supabase.from('table').select()
  // Update UI
})

This ensures users always get updates, with automatic degradation to slower methods during realtime outages.

Frequently asked questions

What does "REALTIME_DISCONNECT: Websocket disconnected from Realtime server" mean?
The "REALTIME_DISCONNECT: Websocket disconnected from Realtime server" error indicates that the WebSocket connection between your application and the Supabase Realtime server has been terminated. Unlike the REALTIME_TIMEOUT error which occurs during initial connection, REALTIME_DISCONNECT happens after a previously established connection drops. The Supabase Realtime service maintains heartbeat messages every 30 seconds to detect connection issues and automatically attempt reconnection with exponential backoff (1s, 2s, 5s, 10s).
How do I fix "REALTIME_DISCONNECT: Websocket disconnected from Realtime server"?
The "REALTIME_DISCONNECT: Websocket disconnected from Realtime server" error occurs when a WebSocket connection to Supabase Realtime unexpectedly closes. This can happen due to network interruptions, server-side timeouts, authentication token expiration, or quota limits. Implementing proper reconnection logic with exponential backoff is essential for maintaining reliable realtime features.
What causes "REALTIME_DISCONNECT: Websocket disconnected from Realtime server"?
Client network loss - WiFi dropout, mobile switching networks, device sleep/lock Corporate firewall/proxy terminating idle WebSocket connections Supabase server idle timeout - connections inactive for extended period
Is the "REALTIME_DISCONNECT: Websocket disconnected from Realtime server" error serious?
It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.

Official resources & further reading