Understanding and Monitoring Realtime Heartbeats

Last edited: 12/12/2025

What are heartbeat messages?

Heartbeat messages are periodic signals sent between the Realtime client and server to verify that the WebSocket connection is alive and functioning properly. The client sends a heartbeat message on the phoenix topic with the event type heartbeat at regular intervals (default: 25 seconds), and the server responds with a phx_reply message.

These messages serve as a keep-alive mechanism to detect connection issues that might not be immediately apparent, such as silent network failures or intermediary proxy timeouts.

Why heartbeats matter

Heartbeat messages are critical for maintaining reliable real-time connections:

  • Connection health monitoring: They detect when a connection has silently failed without triggering WebSocket close events
  • Automatic recovery: When heartbeats time out, the client automatically attempts to reconnect
  • Network proxy compatibility: Many network proxies and load balancers close idle connections; heartbeats keep the connection active
  • Early issue detection: Heartbeat timeouts alert you to connection problems before users experience message delivery failures

Without heartbeats, your application might appear connected while being unable to send or receive messages.

How to handle heartbeat messages

You can monitor heartbeat lifecycle events using the onHeartbeat method:

1
import { useEffect } from 'react'
2
import { createClient } from '@supabase/supabase-js'
3
4
function MyComponent() {
5
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
6
7
useEffect(() => {
8
supabase.realtime.onHeartbeat((status) => {
9
console.log('Heartbeat status:', status)
10
// status can be: 'sent', 'ok', 'error', 'timeout', or 'disconnected'
11
})
12
}, [supabase])
13
14
return <div>Your app content</div>
15
}

Or configure the callback during client initialization:

1
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY, {
2
realtime: {
3
heartbeatCallback: (status) => {
4
console.log('Heartbeat status:', status)
5
},
6
},
7
})

Troubleshooting heartbeat issues

Frequent heartbeat timeouts

If you're seeing frequent timeout status in your heartbeat callback:

  • Network instability: Check your network connection quality
  • Firewall/proxy issues: Corporate firewalls or proxies might interfere with WebSocket connections
  • Mobile app suspension: On mobile, the app can be suspended which can stop the heartbeat timers

Connection not reconnecting after timeout

The client automatically attempts reconnection with exponential backoff (1s, 2s, 5s, 10s). If you need to manually reconnect:

1
import { useState, useEffect } from 'react'
2
import { createClient } from '@supabase/supabase-js'
3
4
function ConnectionStatus() {
5
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
6
const [isConnected, setIsConnected] = useState(true)
7
8
useEffect(() => {
9
supabase.realtime.onHeartbeat((status) => {
10
if (status === 'ok') {
11
setIsConnected(true)
12
} else if (status === 'timeout' || status === 'disconnected') {
13
setIsConnected(false)
14
}
15
})
16
}, [supabase])
17
18
const handleReconnect = () => {
19
supabase.realtime.connect()
20
}
21
22
return (
23
<div>
24
<p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
25
{!isConnected && <button onClick={handleReconnect}>Reconnect</button>}
26
</div>
27
)
28
}

Customizing heartbeat interval

You can adjust the heartbeat frequency when creating your Supabase client in React:

1
import { createClient } from '@supabase/supabase-js'
2
import { useMemo } from 'react'
3
4
function App() {
5
const supabase = useMemo(
6
() =>
7
createClient(SUPABASE_URL, SUPABASE_KEY, {
8
realtime: {
9
heartbeatIntervalMs: 15000, // Send heartbeat every 15 seconds (default: 25000)
10
},
11
}),
12
[]
13
)
14
15
return <YourApp supabase={supabase} />
16
}

Note: Increasing the interval reduces network traffic but delays detection of connection failures. Decreasing it improves detection speed but increases overhead.

Heartbeat errors in Web workers

For React applications with long-running connections, use Web Workers to prevent browser tab throttling:

1
import { createClient } from '@supabase/supabase-js'
2
import { useMemo } from 'react'
3
4
function App() {
5
const supabase = useMemo(
6
() =>
7
createClient(SUPABASE_URL, SUPABASE_KEY, {
8
realtime: {
9
worker: true,
10
workerUrl: '/worker.js', // Optional: Place in public folder
11
},
12
}),
13
[]
14
)
15
16
return <YourApp supabase={supabase} />
17
}

If the worker fails to start, check:

  • Browser Web Worker support is available
  • Worker script is in the public folder and accessible
  • CORS headers allow loading the worker script

Best practices

Show connection status to users

Display connection status with a simple indicator:

1
import { useState, useEffect } from 'react'
2
import { createClient } from '@supabase/supabase-js'
3
4
function ConnectionIndicator() {
5
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
6
const [status, setStatus] = useState('healthy')
7
8
useEffect(() => {
9
supabase.realtime.onHeartbeat((heartbeatStatus) => {
10
if (heartbeatStatus === 'ok') {
11
setStatus('healthy')
12
} else if (heartbeatStatus === 'timeout') {
13
setStatus('poor')
14
} else if (heartbeatStatus === 'disconnected') {
15
setStatus('disconnected')
16
}
17
})
18
}, [supabase])
19
20
return (
21
<div>
22
<span>Connection: {status}</span>
23
</div>
24
)
25
}

Reconnect when React Native app comes to foreground

Ensure connection is active when user opens your app:

1
import { useEffect } from 'react'
2
import { AppState } from 'react-native'
3
import { createClient } from '@supabase/supabase-js'
4
5
function App() {
6
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
7
8
useEffect(() => {
9
const subscription = AppState.addEventListener('change', (nextAppState) => {
10
if (nextAppState === 'active') {
11
if (!supabase.realtime.isConnected()) {
12
supabase.realtime.connect()
13
}
14
}
15
})
16
17
return () => {
18
subscription.remove()
19
}
20
}, [supabase])
21
22
return <YourApp />
23
}

Monitor connection in your entire app

Set up monitoring when you create your Supabase client:

1
import { createClient } from '@supabase/supabase-js'
2
3
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY, {
4
realtime: {
5
heartbeatCallback: (status) => {
6
if (status === 'ok') {
7
console.log('Connection healthy')
8
} else if (status === 'timeout') {
9
console.warn('Connection slow')
10
} else if (status === 'disconnected') {
11
console.error('Disconnected')
12
}
13
},
14
},
15
})
16
17
export default supabase