Skip to content

Broadcast subscription and heartbeat lost randomly without exceptions/error raised #1299

@SmaugPool

Description

@SmaugPool

Describe the bug

After a random numbers of hours (or days), subscription to broadcast get lost and heartbeat doesn't get replies anymore without any subscription error raised. Once this happens, the channel doesn't receive broadcast events anymore.

2025-11-04 10:34:26,436 - INFO - Attempting to connect to WebSocket at wss://xxx.supabase.co/realtime/v1/websocket?apikey=sb_secret_xxx
2025-11-04 10:34:26,538 - INFO - WebSocket connection established successfully
2025-11-04 10:34:26,571 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 10:34:26,576 - INFO - receive: '{"ref":null,"event":"phx_reply","payload":{"status":"ok","response":{}},"topic":"phoenix"}'
2025-11-04 10:34:26,576 - INFO - parsed message as event=<ChannelEvents.reply: 'phx_reply'> topic='phoenix' payload=SuccessReplyMessage(status='ok', response=ReplyPostgresChanges(postgres_changes=None)) ref=None
2025-11-04 10:34:26,740 - INFO - send: {"event":"phx_join","payload":{"config":{"broadcast":null,"presence":null,"private":true,"postgres_changes":[]},"access_token":"sb_secret_xxx"},"topic":"r>
2025-11-04 10:34:26,756 - INFO - receive: '{"ref":"1","event":"phx_reply","payload":{"status":"ok","response":{"postgres_changes":[]}},"topic":"realtime:test"}'
2025-11-04 10:34:26,757 - INFO - parsed message as event=<ChannelEvents.reply: 'phx_reply'> topic='realtime:test' payload=SuccessReplyMessage(status='ok', response=ReplyPostgresChanges(postgres_changes=[])) ref='1'
2025-11-04 10:34:26,757 - INFO - realtime:test : event=<ChannelEvents.reply: 'phx_reply'> topic='realtime:test' payload=SuccessReplyMessage(status='ok', response=ReplyPostgresChanges(postgres_changes=[])) ref='1'    
2025-11-04 10:34:26,757 - WARNING - subscription status=SUBSCRIBED err=None
2025-11-04 10:34:26,757 - INFO - receive: '{"ref":null,"event":"presence_state","payload":{},"topic":"realtime:test"}'
2025-11-04 10:34:26,757 - INFO - parsed message as event=<ChannelEvents.presence_state: 'presence_state'> topic='realtime:test' payload={} ref=None
2025-11-04 10:34:26,757 - INFO - realtime:test : event=<ChannelEvents.presence_state: 'presence_state'> topic='realtime:test' payload={} ref=None    
2025-11-04 10:34:36,742 - WARNING - subscription status=TIMED_OUT err=None
2025-11-04 10:34:51,573 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 10:34:51,578 - INFO - receive: '{"ref":null,"event":"phx_reply","payload":{"status":"ok","response":{}},"topic":"phoenix"}'
2025-11-04 10:34:51,578 - INFO - parsed message as event=<ChannelEvents.reply: 'phx_reply'> topic='phoenix' payload=SuccessReplyMessage(status='ok', response=ReplyPostgresChanges(postgres_changes=None)) ref=None
2025-11-04 10:35:16,575 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 10:35:16,580 - INFO - receive: '{"ref":null,"event":"phx_reply","payload":{"status":"ok","response":{}},"topic":"phoenix"}'
2025-11-04 10:35:16,581 - INFO - parsed message as event=<ChannelEvents.reply: 'phx_reply'> topic='phoenix' payload=SuccessReplyMessage(status='ok', response=ReplyPostgresChanges(postgres_changes=None)) ref=None
2025-11-04 10:35:41,577 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 10:35:41,583 - INFO - receive: '{"ref":null,"event":"phx_reply","payload":{"status":"ok","response":{}},"topic":"phoenix"}'
2025-11-04 10:35:41,583 - INFO - parsed message as event=<ChannelEvents.reply: 'phx_reply'> topic='phoenix' payload=SuccessReplyMessage(status='ok', response=ReplyPostgresChanges(postgres_changes=None)) ref=None
2025-11-04 10:36:06,579 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 10:36:06,590 - INFO - receive: '{"ref":null,"event":"phx_reply","payload":{"status":"ok","response":{}},"topic":"phoenix"}'
2025-11-04 10:36:06,590 - INFO - parsed message as event=<ChannelEvents.reply: 'phx_reply'> topic='phoenix' payload=SuccessReplyMessage(status='ok', response=ReplyPostgresChanges(postgres_changes=None)) ref=None
2025-11-04 10:36:31,582 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 10:36:31,589 - INFO - receive: '{"ref":null,"event":"phx_reply","payload":{"status":"ok","response":{}},"topic":"phoenix"}'
2025-11-04 10:36:31,590 - INFO - parsed message as event=<ChannelEvents.reply: 'phx_reply'> topic='phoenix' payload=SuccessReplyMessage(status='ok', response=ReplyPostgresChanges(postgres_changes=None)) ref=None
...
2025-11-04 13:51:07,387 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:51:07,413 - INFO - receive: '{"ref":null,"event":"phx_reply","payload":{"status":"ok","response":{}},"topic":"phoenix"}'
2025-11-04 13:51:07,413 - INFO - parsed message as event=<ChannelEvents.reply: 'phx_reply'> topic='phoenix' payload=SuccessReplyMessage(status='ok', response=ReplyPostgresChanges(postgres_changes=None)) ref=None
2025-11-04 13:51:32,389 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:51:32,398 - INFO - receive: '{"ref":null,"event":"phx_reply","payload":{"status":"ok","response":{}},"topic":"phoenix"}'
2025-11-04 13:51:32,398 - INFO - parsed message as event=<ChannelEvents.reply: 'phx_reply'> topic='phoenix' payload=SuccessReplyMessage(status='ok', response=ReplyPostgresChanges(postgres_changes=None)) ref=None
2025-11-04 13:51:57,391 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:52:22,392 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:52:47,394 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:53:12,395 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:53:37,397 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:54:02,398 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:54:27,400 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:54:52,401 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:55:17,403 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:55:42,404 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:56:07,406 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:56:32,407 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:56:57,407 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:57:22,409 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:57:47,410 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:58:12,411 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:58:37,413 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:59:02,416 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:59:27,418 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 13:59:52,421 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 14:00:17,424 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 14:00:42,426 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 14:01:07,428 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 14:01:32,430 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 14:01:57,432 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 14:02:22,433 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 14:02:47,435 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 14:03:12,437 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 14:03:37,438 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 14:04:02,440 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 14:04:27,442 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 14:04:52,444 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
2025-11-04 14:05:17,445 - INFO - send: {"event":"heartbeat","payload":{},"topic":"phoenix","ref":null,"join_ref":null}
...

The heartbeart code does not seem to check that a reply is received:

async def _heartbeat(self) -> None:

And there is no heartbeat callback to allow users to detect the issue:

elif isinstance(message, HeartbeatMessage): # do nothing

Reproduction

#!/usr/bin/env python3    
    
import os, asyncio, logging    
from supabase.client import AsyncClient    
    
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")    
    
async def main():    
    def on_subscribe(status, err):    
        logging.warning(f"subscription status={status} err={err}")    
    
    def on_message(msg):    
        logging.info(msg)    
    
    client = AsyncClient(os.environ.get("SUPABASE_URL"), os.environ.get("SUPABASE_KEY"))    
    channel = client.channel("test", {"config": {"private": True}})    
    await channel.on_broadcast("UPDATE", on_message).subscribe(on_subscribe)    
    while True:    
        await asyncio.sleep(1)     
             
asyncio.run(main())

Steps to reproduce

Let the reproduction snippet above run until the issue happens.
It can take minutes, hours or days.

Library affected

realtime

Library version

supabase 2.22.4

Python version

python 3.12.3

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions