You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
AsyncRealtimeChannel.subscribe returns without waiting for an answer #1209
When subscribe is called, the channel object will send the payload through the websocket connection and instantly return, without waiting for an answer on whether subscribe was successful or not. This creates some unexpected behavior, as callback is not guaranteed to be called after await channel.subscribe(callback) returns which create non trivial race conditions.
Because of this, even if the callback crashes the program in the case of failure to subscribe to a channel, API calls after subscribe returns are not guarantee to be in a valid subscribed state, as it may be that callback hasn't been called yet.
To Reproduce
Create a supabase start local instance, then run the example given in the README.md:
# test.pyimportasynciofromtypingimportOptionalfromrealtimeimportAsyncRealtimeClient, RealtimeSubscribeStatesasyncdefmain():
REALTIME_URL="ws://localhost:54321/realtime/v1"API_KEY="{API_KEY}"# api key heresocket=AsyncRealtimeClient(REALTIME_URL, API_KEY)
channel=socket.channel("test-channel")
def_on_subscribe(status: RealtimeSubscribeStates, err: Optional[Exception]):
raiseSystemExit(100)
awaitchannel.subscribe(_on_subscribe)
if__name__=='__main__':
asyncio.run(main())
print('`main()` ran successfully')
Which results in:
$ python test.py
`main()` ran successfully
Even though the callback should've crashed the program instantly.
Expected behavior
After channel.subscribe(callback) is awaited, it should ensure that callback is called by waiting for the answer inside the subscribe async function. Currently, the only way to ensure that it is called is to manually block after the await point with some asyncio.sleep calls, which is not only ugly but also requires you to emulate timeouts externally, even though subscribe does seem to have an internal timeout mechanic.
I believe one possible solution to this is by waiting for the answer inside the function and directly returning the payload from the subscribe call, which would remove the need of passing in a callback. This also would facilitate the user to do direct control flow, by matching on the payload type, instead of the current behavior that forces the user to use some non-local asyncio.Events to handle the control.
For instance, instead of using this style of control flow (present in almost all tests):
which makes control flow easier, and does not duplicate the timeout logic in asyncio.wait_for.
It is important to notice that this would be a breaking change, as it would need to change the type of subscribe. One alternative would be to introduce a new function that subsumes this use case and deprecate subscribe in the future, due to this behavior.
The downside of your suggested approach is that when we call subscribe(), the rejoin logic runs in the background. So, anytime a socket sends an error or closes, it will automatically try to rejoin the channel.
How would we handle that in your suggestion?
But I agree we should rethink how we handle that, we want to change the API for realtime for quite a while.
I believe the auto rejoin and the subscribe callback should be two different concerns. I believe that the current callback based design doesn't give the user enough knowledge and control over what's going on under the curtains, as it isn't at all places that websocket errors may be thrown, just precisely on those that send information to the server. Callbacks are best fit for reacting to server messages, not exactly when you'd like to know the result of a message you sent.
For instance, websocket errors may only happen on send_broadcast, track, subscribe and unsubscribe. These should directly return/raise the result of sending message instead of relying on the callbacks registered on the subscribe method, as currently there's no way to know certainly if a message was correctly sent, due to the AsyncPush class supressing all these errors into the logger:
classAsyncPush:
...
asyncdefsend(self):
# boilerplatetry:
awaitself.channel.socket.send(
{
"topic": self.channel.topic,
"event": self.event,
"payload": self.payload,
"ref": self.ref,
"join_ref": self.channel.join_push.ref,
}
)
exceptExceptionase:
logger.error(f"send push failed: {e}")
# no re-raising of the exception
Isn't guaranteed to being correct, as the first message may error out due to a network error, and the second send may continue through.
Besides that, I don't know if automatic rejoining on the background is a practice we should maintain. At the very least, I believe the user should know about it and opt into that, as it's not clear nowhere in the documentation that it happens. Furthermore, the control flow for generating correct code with it is very much non trivial, as you need to constantly check that the connection you have is valid inside the callbacks.
Activity
o-santi commentedon Jun 26, 2025
I believe one possible solution to this is by waiting for the answer inside the function and directly returning the payload from the
subscribecall, which would remove the need of passing in a callback. This also would facilitate the user to do direct control flow, by matching on the payload type, instead of the current behavior that forces the user to use some non-localasyncio.Events to handle the control.For instance, instead of using this style of control flow (present in almost all
tests):One should be able to write the much simpler:
which makes control flow easier, and does not duplicate the timeout logic in
asyncio.wait_for.It is important to notice that this would be a breaking change, as it would need to change the type of
subscribe. One alternative would be to introduce a new function that subsumes this use case and deprecatesubscribein the future, due to this behavior.grdsdev commentedon Jun 26, 2025
The downside of your suggested approach is that when we call
subscribe(), therejoinlogic runs in the background. So, anytime a socket sends an error or closes, it will automatically try to rejoin the channel.How would we handle that in your suggestion?
But I agree we should rethink how we handle that, we want to change the API for realtime for quite a while.
o-santi commentedon Jun 27, 2025
I believe the auto rejoin and the subscribe callback should be two different concerns. I believe that the current callback based design doesn't give the user enough knowledge and control over what's going on under the curtains, as it isn't at all places that websocket errors may be thrown, just precisely on those that send information to the server. Callbacks are best fit for reacting to server messages, not exactly when you'd like to know the result of a message you sent.
For instance, websocket errors may only happen on
send_broadcast,track,subscribeandunsubscribe. These should directly return/raise the result of sending message instead of relying on the callbacks registered on thesubscribemethod, as currently there's no way to know certainly if a message was correctly sent, due to theAsyncPushclass supressing all these errors into the logger:This means that a simple snippet like:
Isn't guaranteed to being correct, as the first message may error out due to a network error, and the second send may continue through.
Besides that, I don't know if automatic rejoining on the background is a practice we should maintain. At the very least, I believe the user should know about it and opt into that, as it's not clear nowhere in the documentation that it happens. Furthermore, the control flow for generating correct code with it is very much non trivial, as you need to constantly check that the connection you have is valid inside the callbacks.
fix(realtime): make subscribe() await server acknowledgement (supabas…
dimasd-angga commentedon Mar 30, 2026
is this issue still open? im looking for an issue that i can contribute
sap6011 commentedon Jun 11, 2026
Is this issue open? May I work on this?