Silent disconnects when the app is in the background

Realtime stops delivering messages after a browser tab or mobile app sits in the background. Why timers get throttled, and how to use a Web Worker heartbeat, reconnect and refetch.

What you see

A dashboard or chat works fine while you look at it. You switch tabs, lock your phone or minimize the window for a few minutes, come back, and new messages have stopped arriving. There's no error in the console, and sometimes the channel still says SUBSCRIBED.

Why it happens

The Realtime client keeps its WebSocket alive by sending a small heartbeat every 25 seconds. If the server stops hearing heartbeats, it closes the connection.

Browsers save battery by slowing down timers in hidden tabs. After a tab has been hidden for a while, Chrome and others can delay timers to roughly once a minute. The heartbeat misses its slot, the server drops the socket, and the client may not notice until its next timer fires. Mobile operating systems go further and suspend the app's network activity entirely.

Anything sent while the socket was down is gone. Broadcast and Postgres Changes don't queue messages for disconnected clients.

How to fix it

  1. Run the heartbeat in a Web Worker. Workers aren't throttled the same way as page timers. Turn it on when you create the client:

    import { createClient } from '@supabase/supabase-js'
    
    const supabase = createClient(
      import.meta.env.VITE_SUPABASE_URL,
      import.meta.env.VITE_SUPABASE_ANON_KEY,
      {
        realtime: {
          worker: true,
          heartbeatCallback: (status) => {
            if (status === 'disconnected') supabase.realtime.connect()
          },
        },
      }
    )
    

    heartbeatCallback gets the status of each heartbeat, so you can reconnect as soon as one reports a disconnect.

  2. Refetch when the user comes back. Treat Realtime as a stream of updates on top of data you load normally. When the page becomes visible again, reload the latest state:

    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'visible') {
        loadLatestMessages() // a normal supabase.from(...).select() call
      }
    })
    
  3. On mobile, react to app state. In React Native, listen to AppState changes. When the app returns to active, make sure the socket is connected, resubscribe channels if needed, and refetch. Don't try to keep a socket open in the background; the OS won't allow it for long.

  4. Use push notifications for background alerts. If users must hear about events while the app is closed, send a push notification from your backend (for example from an Edge Function). Realtime is for while the app is open.

On SupaHost

Nothing here is SupaHost-specific: the same browser and OS behaviour applies everywhere. Reconnects are cheap on SupaHost, since there are no per-connection or per-message fees. Each project accepts up to 500 channel joins per second, so a wave of clients reconnecting at once is well within limits for most apps.

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us