The problem
A web request has a latency budget of milliseconds, but some work takes far longer: rendering a print-ready PDF, resizing a thousand photos, building a monthly report, or waiting on a slow partner API. Do it inside the request and the user waits while threads and connections pile up behind the slow calls. The platform may also cut the request off: Heroku's router gives up when no response has started within 30 seconds (error H12). Rendering capacity can then only grow by adding web servers, all sized for the heaviest job. Starting a background thread in the web process doesn't fix this either. The work dies with the instance, and nothing stops one busy instance from taking on more than it can finish.
How it works
The Azure Architecture Center describes the style as two roles with a message queue between them:
- The web front end handles client requests. It validates input, does the simple reads and writes itself, puts a message on the queue for anything slow and answers straight away.
- The worker takes messages from the queue and does the resource-intensive, long-running or batch work. It can also run on a schedule, with no request involved.
- Around them sit the services that usually come with the style: one or more databases, a cache for hot reads and session state, a CDN for static content, blob storage for files, an identity provider, and remote services such as email or SMS providers.
Both roles are stateless: anything that has to outlive an instance (sessions, orders, files) lives in the shared stores. That is what lets each role be deployed, scaled, sized and replaced on its own. The queue decouples them in time, so the worker can be slower, busy or briefly down while the web tier keeps taking orders.
| Stays in the request (web front end) | Goes to the worker |
|---|---|
| Validation, authorization, simple reads and writes | CPU- or memory-heavy work: rendering, image and video processing, archives |
| Anything the user must see in this response | Calls to slow or unreliable partners: print services, email and SMS gateways |
| Saving the job and putting it on the queue | Scheduled and batch jobs: clean-ups, imports, reports |
Not everything has to go through the queue. The Azure guidance notes that the front end can handle simple reads and writes directly, and that the worker is optional when an application has no long-running work.
When to use it
- A relatively simple domain with some long-running workflows or batch operations, such as a shop that renders photo books or a SaaS app that imports spreadsheets and builds monthly reports.
- Teams that prefer managed platform services (App Service, Elastic Beanstalk, Cloud Run, Heroku) to running their own infrastructure.
- Work that can finish after the response, because the user can be told about the result later.
When not to use it:
- There is no slow work at all. A plain web application is simpler: a queue and a second deployable would only add moving parts.
- The domain is large and changes fast. With many teams and many features, both roles turn into big shared codebases. Plan for a modular monolith or microservices instead.
- The caller needs the result in the same response. A queue makes the request faster, not the work.
Trade-offs
- The message is a contract between two deployables. The two roles are released separately, so during a rolling deployment old and new versions of each run side by side. Messages written by the old web code are still waiting when the new worker starts. Version the messages, prefer additive changes, have workers ignore fields they don't know, and change a message's shape in expand-and-contract steps.
- Delivery is at least once. A queue hides a message while a worker processes it, and makes it visible again if the worker doesn't delete it in time, so a crash means the job runs twice. With Azure Functions on Storage queues, for example, a host crash leaves the message hidden for the storage service's fixed 10-minute timeout before it reappears. Make jobs idempotent: write results under a deterministic name (
1043.pdf), change state with conditional updates, and pass the order ID as an idempotency key to partners so a rerun doesn't print the book twice. - Poison messages. A message that always fails must not loop forever. Cap the attempts and park it in a dead-letter queue. By default, Azure Functions moves a Storage queue message to
<queue>-poisonafter five failed attempts. An Elastic Beanstalk worker environment retries up toMaxRetriestimes (10 by default) before it dead-letters the message. - The dual-write gap. The web tier saves the order, then sends the message. If it fails in between, the order waits forever. The Azure guidance points to a transactional outbox.
- Hidden coupling. The roles usually share one database schema and a code library. A column renamed for the worker breaks the web tier that still reads it, so treat schema changes as expand-and-contract migrations as well.
- The worker becomes a monolith. Every new background feature (rendering, emails, imports, reports) lands in the same worker, with one release cycle and one scaling setting. A slow import can then hold up the renders.
- Results arrive later. The user gets "being prepared", not the PDF, so the UI and the API need a way to report progress and the result.
Implementation notes
- Tell the user the result. A browser or API client can poll a status endpoint (asynchronous request-reply) or listen on a WebSocket. Systems can receive a webhook, and people get an email or a push notification.
- Scale each role on its own signal. Scale the web tier on request rate, CPU or latency, and the worker tier on queue length. AWS's EC2 Auto Scaling guidance uses a backlog per instance (queue length divided by running instances), compared with what one instance can clear in an acceptable time. Size the roles separately too: small web instances, CPU-heavy workers. See autoscaling, competing consumers and queue-based load leveling.
- Urgent jobs first. Give urgent jobs (a customer waiting at checkout) their own queue and workers, a priority queue, so a nightly import can't delay them.
- Keep state out of the roles. Keep sessions and hot data in a distributed cache (cache-aside). The Twelve-Factor App treats sticky sessions as a violation and suggests a time-expiring store such as Memcached or Redis. Serve static files from a CDN (CDN edge caching). Send large uploads straight to blob storage with a short-lived signed URL (valet key) instead of through the web tier.
- Platform examples (checked in October 2026):
- Azure: the Architecture Center's App Service version uses an App Service web app as the front end and an Azure Functions app as the worker. It adds Service Bus or Storage queues, Azure Managed Redis for session state, a CDN for static content and several data stores. It also suggests separate App Service plans, so the two roles scale independently. WebJobs run background programs inside an App Service app, either continuously or triggered manually or on a CRON schedule. They share that app's instances, so host them in their own app if the worker should scale apart from the web tier.
- AWS: an Elastic Beanstalk worker environment manages an Amazon SQS queue and runs a daemon on each instance. The daemon POSTs every message to your application on
localhost: a200 OKdeletes it, and any other answer returns it to the queue. Acron.yamlfile adds periodic tasks. The .NET on Windows Server platform doesn't support worker environments. - Google Cloud: Cloud Tasks queues tasks and dispatches each one to an HTTP handler on Cloud Run, App Engine or any HTTP endpoint, with rate limits and retries. Delivery is at least once, so handlers must be idempotent.
- Heroku: the
Procfiledeclareswebandworkerprocess types, which scale separately (heroku ps:scale web=2 worker=4). Scheduled jobs run through Heroku Scheduler or a clock process. Heroku moved to a sustaining-engineering model in February 2026: it is still supported, but gets no new features. - The Twelve-Factor App (VIII. Concurrency) makes this a general process model. Web processes take HTTP requests, worker processes run background jobs, and the set of process types with their counts is the process formation.
- The serverless variant. Both roles can be functions: an HTTP-triggered front end, and queue-triggered workers that the platform scales out with the backlog. On consumption-style plans they scale back to zero when the queue is empty. See serverless.
- Beyond the style. When the worker has grown into a pile of unrelated jobs, split it into one queue and one worker per job type, each deployed and scaled on its own. When the domain itself outgrows the style, move to modules with clear boundaries (modular monolith) or to services that own their data (microservices, database per service).