A prototype answers a request and stops. A real product also has to send a password reset at three in the morning, keep the photo somebody uploaded, and send an invoice reminder on the first of next month. None of those fit inside "a page loads". They are the three pieces people discover at the end of the project, when they are hardest to add.
The idea
Some work does not belong in the request. Sending email means handing a message to a service that specializes in getting mail delivered rather than filtered as spam. Storing an upload means putting the file somewhere that is not your app server, because that disk gets wiped on every deploy. Work that runs later means a scheduled job or a A list of jobs your app writes to and a separate worker reads from, so slow work happens after the user's page has already loaded. that lets the visitor's page finish loading while the slow part happens somewhere else.
REQUEST (must finish in ~1s) LATER (nobody is waiting)
+-----------------------------+ +------------------------------+
| user clicks "sign up" | | worker process |
| write row to Postgres | | picks the job off a queue |
| put "send welcome" on ----------> | calls Resend |
| the queue | | retries if it fails |
| return the page | +------------------------------+
+-----------------------------+
+------------------------------+
uploads never touch your app disk | cron: 03:00 daily |
browser --> Supabase Storage / S3 | expire old sessions |
(bucket, private by | email invoice reminders |
default, signed URLs) +------------------------------+How it works
Email is a deliverability problem more than a code problem. Resend and Postmark exist because mail you send from your own server lands in spam. They give you an API call and, more importantly, the DNS records (SPF, DKIM and DMARC) that tell the receiving side your domain really authorized the message. Set those up on day one, because reputation takes weeks to build and one bad send to ruin.
Uploads go to A service built to hold files by name and hand them back over the web. S3 is the original one and most others copy its interface.: Amazon S3, Supabase Storage, Cloudflare R2. Your server never holds the file. It hands the browser a short-lived signed URL, the browser uploads straight to the bucket, and your database stores the path, not the bytes. Keep buckets private and generate a signed link when someone is allowed to read one.
Background work splits in two. Scheduled work runs on a clock, which is A scheduler that runs a job at fixed times, like every night at three. on a server or a scheduled function on Vercel or Supabase. Queued work runs as soon as a worker is free, and the job row is the durable part: written first, marked done last, so a crash means a retry rather than a lost email.
What to do
- Add email early, with a real domain and verified DNS records. Send yourself the welcome mail and check it did not land in spam.
- Send uploads directly to storage from the browser using a signed URL. Store the file path in Postgres. Cap the file size on both sides.
- Move anything slower than about a second out of the request: image resizing, PDF generation, third-party calls that can hang. The user should not wait for it.
- Make every background job safe to run twice. It will run twice.
Where it breaks
Each of these adds a piece that fails on its own schedule, quietly. A queue with no worker running looks exactly like a working app until you notice nobody got their email, so you need visibility into failed jobs, not just successful ones. Storage costs grow without anyone deciding, especially if users upload video and nothing ever expires. Cron on a single server is fine until you run two servers and every job fires twice. And most hosting platforms cut a request off after ten to sixty seconds, so "just do it in the request" stops working the day your task gets slightly slower.