Practical Background Jobs and Queues for Solo SaaS Founders
A practical guide to moving slow work off the request path, operating a small queue, handling failures, and deciding when background-job complexity is justified.
Most early SaaS request handlers do too much. A user clicks “send invoice” and the web process generates a PDF, calls a payment API, writes several database rows, and sends two emails before returning. When any of those steps slow down or fail, the user sees a spinner or an error, and you get a support ticket. Background jobs exist to move that work out of the critical path so the request can finish quickly and the heavy or unreliable parts can be retried separately.
For a solo founder the trade-off is real: a queue adds another process to run, monitor, and debug. The goal is the smallest setup that removes the measured pain without creating a new source of silent failures.
Decide whether you need a queue yet
You do not need one on day one. Synchronous work is simpler to reason about and to debug. Move work off the request only when at least one of these is true:
- A user-facing action regularly takes more than a second or two and the user is waiting (PDF generation, external API enrichment, large export).
- An external service is slow or occasionally down, and you want the user request to succeed even if the later step fails and retries.
- You need to do work that is not tied to a single request (periodic cleanup, scheduled emails, webhook retries you control).
- A spike in one expensive operation is blocking other requests on the same process or connection pool.
If the slow step is rare and the user can tolerate a short wait, keep it synchronous and add a timeout plus a clear error message. A queue that exists only for future-proofing is pure overhead.
Start with the simplest reliable pattern
Two approaches cover most solo-founder needs.
Inline async (fire-and-forget in the same process) for very light work: after you save the database record, spawn a thread or use your language’s async primitive to send an email or call a webhook. This works until the process restarts or the volume grows. It is not durable.
A real queue (Redis + Sidekiq/Bull/Celery, or a managed equivalent such as SQS, Cloud Tasks, or a Postgres-backed job table) when the work must survive restarts and be retried. The pattern is:
- In the web request, validate input, write the durable record (order, invoice, job status), and enqueue a small payload that contains only identifiers.
- Return success to the user immediately (“Your export is being prepared; we will email you when it is ready”).
- The worker process picks up the job, loads the current data by ID, does the work, and updates status or sends the notification.
Illustrative enqueue (adapt to your stack):
- Create the
Exportrow with statuspending. - Enqueue
{ export_id: 123 }. - Worker loads the export, generates the file, stores it, sets status
complete, sends the email.
Never put large objects or user content in the job payload. Pass IDs and let the worker read the current state from the database. That way a retry always sees the latest data and you avoid serializing secrets or large blobs.
Failure handling that stays operable
Jobs fail. External APIs time out, PDFs hit edge cases, databases briefly disconnect. A solo founder cannot sit in a dashboard watching every failure, so the system must be mostly self-healing and loudly visible when it is not.
Practical defaults:
- Automatic retries with exponential backoff for transient errors (network, rate limits, temporary unavailability). Cap the number of attempts (for example 5–8) so a permanently bad job does not retry forever.
- Dead-letter or failed-job visibility. After max retries, move the job to a failed state you can see (Sidekiq dead set, a
failed_jobstable, or an alert). Check it at least daily or alert when the count rises. - Idempotency. Design the worker so running the same job twice is safe. Use a status column (
pending→processing→complete), unique constraints, or a processed-event log. If the job sends an email or charges a card, make sure the second run does not do it again. - Timeouts. Bound every external call and the overall job. A job that hangs forever blocks a worker slot.
When a job fails permanently, the user should still be able to find out what happened (status page, email, or support reply) and, where possible, retry from the UI. Silent failure is the fastest way to lose trust.
Monitoring and operations for one person
You need three signals, not a full observability platform:
- Queue depth or lag (how long jobs wait before being processed).
- Failed-job count or rate.
- Worker process health (is the process running and consuming?).
Most queue libraries and managed services expose these. Set a simple alert: “failed jobs > 10 in the last hour” or “queue lag > 5 minutes.” Review the failed list once a day or after any deploy that touches job code. Keep the worker logs searchable for the job ID so you can reconstruct what happened.
Run the worker as a separate process or container from the web servers. Deploy them together so a code change that affects job logic is released atomically. If the worker is down, new jobs queue up; the web app should still function and show pending status.
What not to put in the queue (yet)
- Work that must finish before the user sees the result and cannot be explained as “processing.” Login, password reset confirmation, or a payment that must succeed before showing the receipt usually stay synchronous (with tight timeouts).
- High-volume, low-value tasks that will fill the queue and starve important jobs. Add prioritization or a separate queue later if this appears.
- Jobs that require human input or long-running interactive sessions. Those belong in a different system.
- Anything you have not measured as a problem. The extra process and failure modes are not free.
Start with one or two job types that clearly improve user experience or reliability. Expand only after those are stable.
Practical checklist before you ship a background job
- The user-facing request finishes quickly and returns a clear status or confirmation.
- The job payload contains only identifiers; the worker loads fresh data.
- The worker is idempotent; running it twice does not double-charge, double-email, or corrupt state.
- Transient failures retry with backoff; permanent failures land in a visible failed state.
- Job duration and external calls are bounded by timeouts.
- You can see queue lag and failed-job count without logging into a server.
- Worker and web processes are deployed together and the worker can be restarted independently.
- A failed job has a path for the user (or you) to understand status and retry where appropriate.
Common pitfalls
- Putting the entire user payload in the job and then discovering the data changed or a secret leaked into logs.
- No idempotency, so a retry sends the customer two identical invoices.
- Letting failed jobs accumulate silently until a customer complains days later.
- Running the worker inside the same process as the web server and then watching a long job block new requests.
- Enqueueing work that depends on an in-memory cache or request-scoped object that no longer exists.
- Adding a queue for every slow endpoint before fixing the underlying query or external call.
When the simple approach is no longer enough
Revisit the design when:
- You need strict ordering or exactly-once semantics across multiple jobs.
- Queue lag or worker CPU becomes a daily problem and simple scaling (more workers) is not enough.
- Different job types interfere with each other and you need priority queues or separate worker pools.
- Compliance or customer requirements demand detailed audit logs of every asynchronous action.
Until then, one durable queue, small ID-only payloads, automatic retries, and a daily glance at the failed-job list will remove most of the user-facing latency and flakiness that early SaaS products hit. Keep the happy path synchronous where it is simple, and move only the work that benefits from being asynchronous.
The best background job is the one whose failure mode you can explain and recover from in a few minutes. Design for that, and the extra process pays for itself.
Sponsor this article · $29 / 30 days
More in Engineering & Architecture
Practical Schema Migrations for Solo SaaS
A practical approach to database schema changes for solo founders: expand-contract patterns, safe deployment order, a...
Practical Caching Strategies for Solo SaaS Founders
A practical guide to deciding when caching is worthwhile, choosing maintainable patterns, handling invalidation, and ...