Bright ideas and techniques for building with Convex.
Profile image
Michal Srb
3 months ago

Wait a minute, won't you? (Add a waitlist to your product)

example of waiting customers

Certain web applications can have a limit to the number of users they can satisfy at a time. To ensure that at least some users can be served, these apps need to protect themselves against surges in demand. A waitlist is a user-friendly way to achieve this. Read on to learn how you can add a waitlist to your app!

Pressed for time? Grab the code on GitHub.

Run-through of the user experience

Let’s look at an example. We’re using the demo app functionality from npm convex create , which we’ve added a waitlist to. When there aren’t many users, your app should behave normally.

standard convex demo app

Once there are too many users, we’ll let the newcomers know that they need to wait. They’ll be shown their estimated position in line and approximately how many people are waiting:

ui when user is on the waitlist

And once there is “space”, they are let in.

You can play with a live demo here. It’s been configured to allow only 3 users at a time to have access. You can open multiple browser tabs to create multiple user sessions.

Installation instructions

Can’t wait to get started? Follow the README to learn how to add a waitlist to your existing app. The README also covers all available configuration. You don’t need to use Convex for anything else other than the waitlist (but you really should).

get-convex/convex-waitlist

An easy to adopt waitlist implementation using Convex.

How does it work?

This waitlist implementation has been optimized to:

  1. Handle tens of thousands of users per hour
  2. Minimize the resources (and hence cost) of running the waitlist

…So it’s got a few tricks in it.

Waiting and activating

diagram of waitlist sessions

Let’s start from the basics:

Clients (web browsers, mobile/desktop apps, etc.) will be talking to our backend (server) where the waitlist implementation is deployed. In this case the backend will be hosted on Convex.

The client identifies each user session with a string sessionId, sent to the backend. This string could be a user ID, if the app includes sign-in before users should be put on the waitlist.

The backend sends back to the client the session’s current status: “waiting” or “active”, and its position, which is used to show the user’s position in the waiting line, which we’ll cover later. Convex is a reactive backend, so if any of this data changes, it will update the client with the new values.

The backend is configured to only allow a certain number of active sessions at one time. Once that limit is reached any new sessions start off with the waiting status.

Besides the status and position, the backend stores a lastActive timestamp for every session. For active sessions, the timestamp can be refreshed every time the user actively uses the app. The implementation provides a validateSessionAndRefreshLastActive function for this, which updates the timestamp to the current time. For waiting sessions the lastActive timestamp is initially null, and it only gets set when a user leaves (we’ll also cover this later).

Periodically, say every minute, a cron job runs on the backend that updates the whole waitlist.

  1. First, it checks whether there are any waiting sessions. If there aren’t any, it’s done.
  2. Then it deletes any stale waiting sessions.
    1. Stale sessions have a non-null lastActive timestamp older than the configured timeout
  3. Then it deletes any stale active sessions.
  4. Then it marks waiting sessions and makes them active , up to the number of stale sessions it just deleted.
  5. Finally it updates the active sessions counter.

This counter is used when a new session is created to determine the session status (as mentioned above). If the newly created session starts off as active the counter is also incremented.

detailed diagram of updateWaitlist scenario

As a final note, any endpoints reading data (in Convex these are called “queries”) should be protected to only serve active sessions by checking the current session status. The implementation provides a validateSessionIsActive function that does this and throws an error if the session isn’t active.

The waitlist is optimized to handle a LOT of users. When a new user comes in, the backend needs to create a waitlist session for them, but to create a session for them it needs to know how many active sessions there are already. The implementation could:

  1. Count the number of active sessions every time.
  2. Keep a count of the active sessions in a separate table.

The first approach works fine for dozens of users, but would be slow for thousands. The second approach is better because it doesn’t waste resources on counting the number of sessions again and again. It does entail duplicating a piece of state though - the number of active sessions - but thanks to Convex’s transactional guarantees, you don’t have to worry about the two getting out of sync, as long as the implementation always updates both in the same transaction (in Convex every mutation is a transaction).

Showing the position in the line to the user

So far we’ve covered how the backend works efficiently to determine each session’s status. But the implementation also takes care to efficiently update all the clients about their position in the line.

Imagine that the backend sent to each client what their actual position is, say ”1st out of 100”. Every time a new waiting session is created, all the clients would need to receive a custom update about the line size! Even if we didn’t show the number of users waiting, every time a session becomes active, all the waiting sessions behind it in the line would need a tailored update. That’s up to 1000 server function calls when there are 1000 users waiting, on every update!

Clearly this approach is not cheap. We can do better by offloading a little math to the client-side.

We mentioned before that each session receives from the backend the current status (“waiting” or “active”) and its position. This is not the position among the currently waiting sessions, but a globally incrementing position. The first session ever gets assigned position 0, the second position 1, and so on.1

Besides this session specific information, which only changes when the user gets off of the waitlist, the backend also sends to all clients “global” information about the waitlist: firstWaitingPosition and lastWaitingPosition. This data does change every time a session becomes active or when a new waiting session is created, but because it doesn’t vary between clients, Convex can cache this result, so we’re only hitting the database once, even when the result gets sent to a 1000 clients.

Finally, given the firstWaitingPosition, lastWaitingPosition and the current session’s position the client can easily compute its position in the waiting line and how many users in total are waiting.

diagram of the information clients receive

A careful reader such as yourself might spot a little issue with this increment-based approach to estimating the position in the waiting line. What if some people give up and leave while they’re still waiting?

This is another trade-off the implementation makes. To show an accurate position in the line, the backend would have to count (or a keep a count of) how many waiting sessions there are in front of each waiting session, or recalculate the position of each waiting session every time someone with lower position leaves. But the implementation is designed to support a LOT of users cheaply, especially those on the waitlist. And hence the waitlist forgoes a little precision to keep things efficient. In practice, with a large number of active and waiting sessions, this estimate should still give the user a good idea of where they stand.

Leaving while waiting

We mentioned before that active sessions become stale when the user stops interacting with your app. What about waiting sessions though? The waiting users should not have to interact with the app to stay on the waitlist. In fact there should be no additional traffic from the waiting users to keep the cost of the waitlist as low as possible. Instead the implementation takes an “optimistic” approach: The client sends a request when the user navigates away from the app, notifying the server. This is when lastActive timestamp gets set. If the user doesn’t come back to the app within the configured timeout period, the waiting session becomes stale and is deleted.

Conclusion

We hope you’ll find this waitlist implementation helpful. It should be easy to adopt, and build upon. For example, you could track the rate at which active sessions become stale to show an estimated waiting time to users in line. Let us know what other features you’d like a waitlist to have and how you’re taking advantage of Convex. Happy waiting.

get-convex/convex-waitlist

An easy to adopt waitlist implementation using Convex.

Footnotes

  1. The implementation uses 64bit floats to represent the integer position, which is perhaps suboptimal, but still allows for over 9 million billion sessions - that’s 30K sessions per second for 10 thousand years. Convex does support 64bit integers though, so this limit can be increased easily. Do let us know if you’re planning to support more than 10^15 sessions, we’d love to hear about your app!

Build in minutes, scale forever.

Convex is the backend application platform with everything you need to build your project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.

Get started