Bogdan
Bogdan16mo ago

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
jamwt
jamwt16mo ago
@Bogdan is this for presence?
Bogdan
BogdanOP16mo ago
essentially more like lack there of, what i'm trying to do is delete data from the db when users leave
jamwt
jamwt16mo ago
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.
jamwt
jamwt16mo ago
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
Bogdan
BogdanOP16mo ago
this looks like exactly what i'm looking for thanks didnt know the technical term for that i was trying to descibe was presence
jamwt
jamwt16mo ago
no problem. happy to help
Bogdan
BogdanOP16mo ago
do you have any advice on how to approach this other than storing and constantly reading/writing presence data? its quite resource-intensive.
ian
ian16mo ago
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.
Bogdan
BogdanOP16mo ago
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
jamwt
jamwt16mo ago
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 mutation
jamwt
jamwt16mo ago
Navigator: 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.
Bogdan
BogdanOP16mo ago
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
jamwt
jamwt16mo ago
HTTP Actions | Convex Developer Hub
HTTP actions allow you to build an HTTP API right in Convex!
jamwt
jamwt16mo ago
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
Bogdan
BogdanOP16mo ago
thank u for pointing this out u guys r great
jamwt
jamwt16mo ago
awesome! glad to help let us know how it goes
LeoCaprilin
LeoCaprilin16mo ago
Tested the sendBeacon approach with the http actions, works really well !!
Bogdan
BogdanOP16mo ago
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
LeoCaprilin
LeoCaprilin16mo ago
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.
Heath
Heath4mo ago
@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?
jamwt
jamwt4mo ago
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
Heath
Heath4mo ago
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
jamwt
jamwt4mo ago
hmm. interesting! we'll have to work on a replication here
Heath
Heath4mo ago
I wouldn't be surprised if Pubnub is using all these things at once
jamwt
jamwt4mo ago
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.
Heath
Heath4mo ago
yes, exactly.
jamwt
jamwt4mo ago
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
Heath
Heath4mo ago
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.
jamwt
jamwt4mo ago
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
jamwt
jamwt4mo ago
(web standards are so much fun... lol)
Heath
Heath4mo ago
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)
sujayakar
sujayakar4mo ago
@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.
Heath
Heath4mo ago
@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.
sujayakar
sujayakar4mo ago
of course! let me know if y’all run into any other questions like this — always happy to chat.
ampp
ampp4mo ago
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
sujayakar
sujayakar4mo ago
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.

Did you find this page helpful?