Handle client disconnecting?
On the backend, how would i listen for a client disconnecting?
With socket.io i could just add a socket.on("disconnect", callbackFn()) to handle this, is there something similar built into convex?
i thought about using beforeunload but it's unreliable.
any ideas or suggestions?
36 Replies
@Bogdan is this for presence?
essentially
more like lack there of, what i'm trying to do is delete data from the db when users leave
https://stack.convex.dev/presence-with-convex <- that has a lot of patterns and a conversation around this
Implementing Presence with Convex
Some patterns for incorporating presence into a web app leveraging Convex, and sharing some tips & utilities I built along the way.
socket disconnection alone is unreliable over time due to backend restarts, distributed connections in the backend cluster, etc. convex is a bit more involved than one server handling all your sockets that will not restart or upgrade or hand a websocket off to another host and so on
this looks like exactly what i'm looking for thanks
didnt know the technical term for that i was trying to descibe was presence
no problem. happy to help
do you have any advice on how to approach this other than storing and constantly reading/writing presence data? its quite resource-intensive.
I have an idea for how to decrease the number of query invalidations if you're interested, but it still requires doing a heartbeat that updates a lastSeen time. We've talked about exposing client connection / disconnection events on the server, but have not started that effort.
yes im all ears. honestly the most ive thought about it is reducing the time between heartbeats but that would just get rid of that nice instant responsiveness
as my app stands right now implementing presence with heartbeats will be the feature that throws me over the edge function call and bandwith limits wise
there are strategies to fire a high-probability unload triggered eager-signoff message and then you can lengthen your heartbeat window because the periodic cleanup only has to handle the cases where this didn't work. you could use
sendBeacon
with an httpAction, or just use onunload
or onbeforeunload
to call one last mutationNavigator: sendBeacon() method - Web APIs | MDN
The navigator.sendBeacon()
method asynchronously sends an HTTP POST request containing a small amount of data to a web server.
in my testing onunload and onbeforeunload worked like 1% of the time
and i instantly disreguarded sendBecon because i was fully convinced in the moment there was no http api endpoint
http actions riiiight
i should j read the docs for a good few days
HTTP Actions | Convex Developer Hub
HTTP actions allow you to build an HTTP API right in Convex!
yep, if you want high reliability, sendBeacon with an HTTP action, and then make your ping times like a minute or whatever
then have a cron clean stuff up that's > 60 + 10 seconds old
that will look pretty darn good in practice and not be very traffic heavy
and it will be robust against backend restarts and all manner of other deficiencies of classic "socket disconnect" type strategies
thank u for pointing this out u guys r great
awesome! glad to help
let us know how it goes
Tested the sendBeacon approach with the http actions, works really well !!
Yeah I did too before I went home saw no issues with it, excited to fully implement it
There probably will be cases where it doesn’t reach the server in time but I couldn’t get it to fail
I made it fail opening a lot of tabs and right click "close all tabs to the right", but just didn't erase one user, but I think that is a picky case.
@ian It would be valuable to have access to client connection/disconnection events on the server. This would allow us to build presence functionality that is matching how some more specialized vendors handle presence. For example here's how Pubnub describes how their websocket is used to track leave events:
Generate Leave on TCP FIN or RST This option will detect the connection termination of a network layer and report it as a PubNub leave event. A leave event instead of a timeout event will be generated when a browser tab is closed or an application is force quitted.Without that I believe we are left depending on unload events (can be unreliable) or a heartbeat timeout (fine as a failover but not for snappy presence). Let me know though if there's something I'm missing. @LeoCaprilin I saw you are using sendBeacon, but which event are you using to fire the beacon?
TCP FIN or RST isn't too reliable either, unfortunately
unless you control both endpoints and can configure the tcp keepalive behavior reliably
the combo we use is the beacon + a timeout, which amounts to about as well as TCP will give you in practice unless you have tight control over the network
in our experience, it's pretty reliable
this is in production with a pretty busy presence/waiting list system on convex for around 10-11 months now
Hey @jamwt, really appreciate the response!
I implemented a Beacon (w/ beforeunload) today + timeout, and was just comparing the effectiveness to our main product which is using Pubnub, and came across some edge conditions that didn't work with the beacon. The main one I noticed was w/ Firefox where if I do a page transition (for instance typing a new URL into the URL bar) it won't fire the beacon
hmm. interesting!
we'll have to work on a replication here
I wouldn't be surprised if Pubnub is using all these things at once
MDN Web Docs
Beacon API - Web APIs | MDN
The Beacon API is used to send an asynchronous and non-blocking request to a web server. The request does not expect a response. Unlike requests made using XMLHttpRequest or the Fetch API, the browser guarantees to initiate beacon requests before the page is unloaded and to run them to completion.
yes, exactly.
cool. yeah, there's like a longer consideration about socket state representing presence that's a little tough b/c (1) network transient errors and (2) the slowness and unreliable nature of TCP FIN, which is why heartbeat-type approaches are usually the fallback. but yeah, if firefox routinely doesn't get its beacon out, you don't want to fall back on the timeouts that routinely
but in a distributed system like convex, the presence of a particular socket is treated as not very special b/c there are a lot of complications around this, especially with infrastructure redundancy, rolling out updates, etc
ok makes sense. tbh I don't know that it's "regularly" since it's much more common for people to just close tab or reload page. I think I will need to do a careful comparison of worse clients though, particularly Safari and mobile.
are you doing unload or pagehide? https://nicj.net/beaconing-in-practice-an-update-on-reliability-and-the-pending-beacon-api/#beaconing-in-practice-update-pagehide-or-visibilitychange
Nic
NicJ.net
Beaconing in Practice: An Update on Reliability and the Pending Bea...
Table of ContentsIntroduction
Pending Beacon API
Why Pending Beacons?
Pending Beacon ExperimentsMethodology
Reliability of XMLHttpRequest vs. sendBeacon() vs. Pending Beacon in Event Handlersonload
pagehide or visibilitychange
onload or pagehide or visibilitychange
ConclusionReliability of P
(web standards are so much fun... lol)
Doing beforeunload but I dabbled with both unload and pagehide today and it didn’t help (but may be worth a revisit now that my implementation stabilized)
@Heath, one other thing to note is that we charge less for database bandwidth now compared to the stack presence article. more specifically, we used to round up each database operation to the nearest kilobyte, which made small reads and writes more expensive. so, a simple client-driven heartbeat system is a lot cheaper than it used to be.
we're currently working on a "scale" plan that will make these patterns even cheaper, since most of the cost is associated with function calls, not database bandwidth. more details are coming soon, but I did some back of the envelope math, and it's roughly ~5c per month of active user time (i.e. a user being online 24/7 for a month, not just being a MAU).
- let's assume the client sends a heartbeat every 5 minutes, so in the case when the beacon API doesn't work, it'll take 5m for the disconnected session to disappear.
- 1 mutation every 5 minutes is 8928 mutations per month. at $2 for 1M mutations, that's 1.8c in function calls per month of active user time.
- I wrote a small heartbeat mutation as a test, and it reads 228 bytes and writes 147 bytes per invocation. at $0.20/GiB of bandwidth, this is 0.06c in db bandwidth per month of user time.
- each mutation will trigger a query to recompute presence state and then a bunch of cached queries. on the scale plan, this will roughly be doubling the function call cost: another 1.8c.
so, I think keeping it simple and having presence all be client driven can be quite affordable at scale on convex. I'll think through some ideas to have our servers push out events when clients disconnect, but I suspect it won't be substantially more efficient than the client-driven architecture here.
@sujayakar thank you very much for such a comprehensive response. We are prototyping right now on some greenfield work, so a lot of this discussion is premature, but nonetheless I am comparing some things as we go to our larger main product and it's valuable to have data points like the ones you've shared.
of course! let me know if y’all run into any other questions like this — always happy to chat.
Yeah those are best case numbers. I'm really trying to cut my heart beat down, its a little more complex at 475 / 342 but i removed a id field so i'm down to 1 and its 397, 303
with a count, id and lastHb time.
I tried shrinking the table name and renaming a index with no help. I'm probably going a little overboard heavily optimizing our heartbeat for a the hackathon when i really need to do other things lol 😅 But it would be great to have a record size calculator. Any tips on removing what characters from where would save the most? Thanks
you could try removing the
count
field (I just needed last heartbeat in my experiment), but I think it's fine to leave it as is since database bandwidth & storage are such a small fraction (~1%) of the total cost.