A shared music playlist where everyone votes, and the order updates for the whole room at once.
Every shared space has the same small problem. One person controls the music, everyone else has opinions, and there is no fair way to resolve them. I wanted a playlist that belonged to the room instead of to a person: anyone can add a track, anyone can vote, and the queue reorders itself based on what the group actually wants.
The hard requirement was that it had to feel live. If you vote and the order does not visibly shift for a beat, the whole thing feels broken. So the real target was not a feature list. It was latency.
This was my first real-time project. I had built plenty of request-response APIs. A system where the server pushes to clients, and clients have to agree on shared state, is a different shape of problem.
Queued is a Node backend with Socket.IO for the real-time layer, PostgreSQL as the source of truth, and Redis as a cache in front of it. Tracks are searched and added through the YouTube Data API. Each track in the playlist carries a vote count, and the playlist is ordered as a priority queue keyed on that count.
POST /api/movies/compare runs in the foreground (acquires lock, writes ratings, COMMIT), while GET /api/movies/pair prefetches the next matchup in parallel via React Query. Perceived latency = compare RTT only. CORS maxAge=86400 eliminates the OPTIONS preflight overhead.
When someone votes, the server updates the count, recomputes the order, and pushes the new playlist to every client in that room over Socket.IO. There is no polling and no manual refresh. The client that voted and the five clients that did not all see the same update at the same time.
On a local test with around 50 concurrent clients, a vote propagated to every client in roughly 80 milliseconds. That is the number that decides whether the room feels alive, and 80ms is comfortably under the threshold where a delay becomes noticeable.
The first version read the playlist straight from PostgreSQL on every vote and every page load. That works until a busy room. The playlist for an active room gets read constantly, far more often than it changes, and every one of those reads was hitting the database.
So the ordered playlist for each room is cached in Redis. Reads hit Redis first. A write, which means a vote, updates Postgres and then invalidates the cache so the next read rebuilds it from fresh data.
The same cache helped a second, less obvious problem. The YouTube Data API has a daily quota. Caching track metadata instead of refetching it on every render roughly halved the quota consumption, which is the difference between the app working all day and the app dying at 4pm.
A shared playlist is a shared counter, and shared counters have the classic race. Two people vote for the same track at the same moment. Both read a count of 7, both write 8, and one vote silently vanishes.
Vote writes run as ACID transactions in PostgreSQL, with the count updated atomically rather than read into the application and written back. To prove it actually held up, I load-tested the vote endpoint with k6, firing concurrent votes at around 500 requests per second on a local setup. The recorded vote totals matched the number of requests sent. Nothing was lost under contention.
These are local numbers, not production traffic. The point of the k6 run was not to publish a benchmark. It was to catch the lost-vote bug before a real room of people did.
Doing the increment inside the database, in one statement, means there is no window between reading and writing for another transaction to slip into. The YouTube integration uses OAuth 2.0 so that track search runs against the API without the client ever seeing server credentials.
I expected the hard part of Queued to be Socket.IO. It was not. Pushing events to a room is straightforward once it clicks. The hard part was everything underneath: making sure the state being pushed was correct, and making sure reading that state did not melt the database.
The lesson that stuck: real-time does not mean recompute everything on every event. It means know exactly what changed, update only that, cache the rest, and broadcast the smallest update you can. The latency target was met by caching aggressively, not by pushing harder.
If I rebuilt it today I would add reconnection handling for clients whose tab slept, and an event log so a client coming back can catch up cleanly instead of refetching the whole playlist. That gap is the subject of one of the posts on my writing page.