I have a small music app that I built for myself. It's a PWA that plays the tracks I've collected, with the audio stored in the cloud and a simple library on top. Playing music was never the hard part. Adding a track was. Each new track needs a slow job: fetch it, convert it, upload it, then add it to the library.
The old setup: the app called my computer
That slow job ran on my home computer. A small API server sat on it, exposed to the internet through a tunnel, and the app called it directly whenever I added a track.
It worked, but it had some real problems:
- The computer had to be on. If it was off, adding a track just failed, and I had to try again once I was home.
- There was a server open to the internet. It was a server on my home network that I had to keep locked down.
- The progress bar was fake. The app only knew the request had been sent. The rest was an animation.
- Errors disappeared. When an upload credential expired, uploads failed quietly for a while. The only sign was a line in a console nobody was watching.
The new setup: the database is the queue
The fix came down to one change: the app no longer calls the worker. It writes a job into a table, and the worker comes to collect it.
- The worker only makes outgoing connections, to the database and to storage. No tunnel, and nothing open to the internet.
- If the computer is off, jobs simply wait in the table. When it's back on, the worker picks them up one by one.
- As it works, the worker writes its progress back into the job, and the app shows it live. The progress bar is real now.
- The worker also checks in regularly, so the app can show whether the worker is online.
Postgres already has what you need to hand out jobs safely: a worker can claim the next waiting job in a way that no other worker can claim it at the same time. If a worker dies halfway through, the job is released after a while and tried again, a few times at most, before it's marked as failed with a Retry button in the app. No Redis or separate queue service, and the free tier of my database covers it.
A queue is something people can write to
Moving the work into a table brings up a new question: who is allowed to write to it? Whatever lands in the table, the worker will do. So:
- Only approved accounts can add jobs, and only for themselves.
- Every job is validated twice: by the database when it's inserted, and again by the worker before it runs. Only the kind of link the app expects is accepted. Without that, a job could ask the worker to fetch an address on my home network.
- A job can only touch the person's own library, even though the worker itself has broader permissions.
What changed
- My computer can be off. It used to mean "the app is broken". Now it just means "new tracks show up a bit later".
- Nothing is open to the internet. The worker only reaches out.
- Progress and errors are real. If something fails, the reason shows up in the app, with a Retry button.
- No new services. The database I already had does the job.
Using a database as a queue isn't a new idea. For a small personal project that you only use now and then, though, it's just enough: it turned "my computer must always be on" from a hard requirement into something I don't have to think about.
Cover: Old vinyl records lined shelf by Unknown, CC0 1.0.
Top comments (0)