Synchronous vs Asynchronous Tasks
Table of Contents
Understanding Task Execution Styles
When you build backend systems, you constantly make a choice: should this work run now and block the user until it is done, or can it run later in the background?
That choice is the heart of synchronous vs asynchronous tasks.
In this chapter you will:
- Understand what "synchronous" and "asynchronous" really mean in backend work.
- See how they affect user experience, performance, and scalability.
- Learn when to choose each style, with concrete examples.
We will not go deep into async programming syntax or tools here, those topics appear in other chapters. Here we focus on concepts and architecture.
Key idea:
A synchronous task blocks and makes the caller wait until it is finished.
An asynchronous task is started, but the caller does not wait for it to finish.
What Is a Synchronous Task?
A synchronous task is work that runs from start to finish in one continuous flow, and the caller waits for it to complete.
In a web backend, this usually means:
- The client sends a request.
- The server starts handling it.
- The server does all the work before sending the response.
- The client waits the whole time.
Simple synchronous flow
Imagine a user signs up:
- User submits the signup form.
- Server:
- Validates data.
- Saves the user in the database.
- Sends a welcome email.
- Only after the email is sent, the server returns
201 Created.
If sending the email takes 2 seconds, then the whole request might take 2.2 seconds. The user sees a spinning loader for 2.2 seconds.
Synchronous example in pseudocode
def register_user(request):
user_data = request.body
validate(user_data)
user = save_user_to_db(user_data)
send_welcome_email(user.email) # This may take a long time
return {"status": "ok", "user_id": user.id}Here, the HTTP response waits for the email to finish sending.
Pros of synchronous tasks
- Simple to understand: one straight line from start to finish.
- Easy to debug: the entire flow happens in one process.
- Good for short operations: database reads, small calculations, quick writes.
Cons of synchronous tasks
- User must wait for slow operations (file uploads, external API calls, emails).
- Server resources are held while waiting, which limits scalability.
- One slow external service can slow down all requests.
Synchronous makes sense when:
- The job is fast enough that waiting is acceptable.
- The client must know the result immediately (for example, login success, payment confirmation, checking permissions).
What Is an Asynchronous Task?
An asynchronous task is work that can:
- Be started now.
- Run separately from the main request.
- Complete later, sometimes after the HTTP response has already been sent.
In backend systems, "asynchronous" usually combines two ideas:
- Non-blocking I/O or async programming (technical side).
- Background task execution using a queue and workers (architectural side).
Here we focus on the second: tasks that run in the background, outside the immediate HTTP request.
Asynchronous flow for the same signup
Same user signup, but handled asynchronously:
- User submits the signup form.
- Server:
- Validates data.
- Saves the user in the database.
- Queues a background job to send the welcome email.
- Returns
201 Createdimmediately. - A worker process later reads the queued job and sends the email.
So the user sees a fast response, 0.2 seconds, instead of 2.2 seconds.
Asynchronous example in pseudocode
def register_user(request):
user_data = request.body
validate(user_data)
user = save_user_to_db(user_data)
# Do NOT send email here.
# Instead, enqueue a background job.
enqueue_job("send_welcome_email", {"email": user.email})
return {"status": "ok", "user_id": user.id}
def worker_loop():
while True:
job = get_next_job()
if job.name == "send_welcome_email":
send_welcome_email(job.payload["email"])The HTTP request finishes quickly, and the email is handled later.
Pros of asynchronous tasks
- Fast responses for the user, even if some work is heavy.
- Better scalability: workers can process many tasks in parallel.
- Fault isolation: if email sending fails, it does not break the main API.
- Retries and scheduling: you can retry failed jobs, or run them at a later time.
Cons of asynchronous tasks
- More complex architecture: you need queues, workers, monitoring.
- Eventual completion: you cannot always give the final result immediately.
- More things can go wrong: lost messages, worker crashes, duplicates.
Async makes sense when:
- Work is slow or heavy: generating PDFs, resizing images, sending many emails.
- Work uses unreliable external services: payment gateways, email providers.
- The client does not need immediate final results.
Comparing Synchronous vs Asynchronous
Think of a restaurant:
- Synchronous: One chef cooks the entire meal from start to finish before starting the next order. If one dish takes 20 minutes, the next customers wait.
- Asynchronous: One waiter takes orders quickly, then different kitchen staff and stations handle parts of the meal in parallel. Customers get drinks, bread, and status updates while the main dish cooks.
Side‑by‑side comparison
| Aspect | Synchronous | Asynchronous |
|---|---|---|
| Response time to user | Slower for heavy tasks | Fast, heavy work runs in background |
| Implementation complexity | Simple | More complex (queues, workers, monitoring) |
| Resource usage | Request holds resources until done | Work can be spread across many workers |
| Error handling | Immediate, simple try/except | Requires retries, dead-letter queues, status tracking |
| Suitable for | Short tasks, must-know-now results | Long tasks, bulk work, unreliable external services |
| Example | Login, balance check, simple CRUD write | Send emails, process images, monthly reports, webhooks |
Impact on User Experience
Backend decisions directly affect what users feel.
Synchronous UX example
A photo-sharing app uploads a photo and generates several thumbnails.
Synchronous approach:
- User uploads photo.
- Server:
- Stores original file.
- Generates 5 thumbnail sizes.
- Only when all thumbnails are ready, response returns.
Users see a long spinner for several seconds before the page loads.
Asynchronous UX example
Same feature, asynchronous:
- User uploads photo.
- Server:
- Stores original file.
- Queues task to generate thumbnails.
- Returns response immediately.
- Client:
- Shows the original or a "processing" state.
- Polls or uses WebSockets to know when thumbnails are ready.
Users see almost instant feedback and can continue browsing.
Important rule:
Use asynchronous background tasks for operations that are:
- Slow or resource intensive,
- Not required for the immediate response, and
- Safe to finish a bit later.
Examples of such operations:
- Sending emails or SMS.
- Processing images or videos.
- Generating reports.
- Syncing data with third party APIs.
- Cleaning up old records or logs.
CPU‑Bound vs I/O‑Bound Work
Not all tasks are slow for the same reason. Understanding why something is slow helps you decide how to run it.
I/O‑bound tasks
These tasks spend most of their time waiting for something external:
- Waiting for database queries.
- Waiting for HTTP calls to other APIs.
- Waiting for file system or network.
I/O‑bound tasks are good candidates for:
- Asynchronous I/O inside a request (async/await).
- Background tasks and job queues.
CPU‑bound tasks
These tasks spend most of their time calculating:
- Image resizing.
- Video encoding.
- Complex statistical calculations.
- Encryption at large scale.
CPU‑bound tasks are good candidates for:
- Background workers, possibly on dedicated machines.
- Limiting concurrency so one worker does not overload the CPU.
Guideline:
- If a task is I/O‑bound and long, prefer async or background jobs.
- If a task is CPU‑bound and heavy, prefer background jobs, often with separate worker processes.
Synchronous Tasks Inside a Single Request
You will still use synchronous logic all the time, even in modern async frameworks.
Typical synchronous operations inside a single HTTP request:
- Parse and validate request body.
- Perform basic authorization checks.
- Run 1 or 2 small database queries.
- Return a JSON response.
Example: fetch user profile
def get_profile(request, user_id):
user = db.get_user(user_id) # small DB query
if user is None:
return {"error": "not found"}, 404
return {"id": user.id, "name": user.name}This entire flow can safely be synchronous inside one request. No need for background tasks.
Asynchronous Tasks with Queues and Workers
In backend systems, asynchronous tasks often involve these components:
- Producer: The part of your app that decides a background job is needed.
- Queue: Where jobs are stored temporarily.
- Worker: A separate process that reads jobs and executes them.
Example flow: generating a PDF invoice
- Client:
POST /invoiceswith order details. - API server:
- Stores invoice record in database.
- Queues a job:
"generate_invoice_pdf", {"invoice_id": 123}. - Returns
202 Acceptedor201 Created. - Worker:
- Reads job from queue.
- Generates PDF, stores it in object storage.
- Updates invoice record with PDF URL.
- Client:
- Polls
GET /invoices/123or uses notifications to see when PDF is ready.
This is a very common pattern in real systems.
Choosing Between Synchronous and Asynchronous
You choose per operation, not per project. The same application will use both.
Ask these questions for each piece of work:
- Does the user need the result right now to continue?
- Yes: probably synchronous.
- No: candidate for async job.
- How long does it take?
- Under ~100 ms: usually fine in the main request.
- Hundreds of ms to seconds: consider async, especially under load.
- What happens if it fails?
- Must succeed immediately: maybe synchronous with retries.
- Can be retried later: good for async processing with retry strategies.
- How heavy is it on resources?
- CPU heavy or uses a lot of memory: background worker, controlled concurrency.
- Light operations: keep synchronous.
Common examples
| Operation | Typical Choice | Reason |
|---|---|---|
| User login | Synchronous | Must know result immediately |
| Fetch product list | Synchronous | Must display now |
| Send "welcome" email | Asynchronous | Can be delayed, may be slow |
| Generate weekly report | Asynchronous | Heavy, can be scheduled at night |
| Resize uploaded images | Asynchronous | CPU heavy, can finish after upload |
| Payment authorization | Synchronous | User must know if payment went through |
| Rebuilding search index | Asynchronous | Very heavy, not tied to a single request |
Simple Patterns You Will Use Often
Here are three very simple architectural patterns that use both synchronous and asynchronous tasks.
1. Fire‑and‑forget background job
Use when you do not need to report back to the user about the job status.
Example: log extra analytics data.
- API request: store main data, enqueue analytics job, return
200 OK. - Worker: logs analytics data to some external system.
2. Background job with polling
Use when the user eventually needs the result.
- API: start the job, return a job ID.
- Client: calls
GET /jobs/{id}to check status. - Worker: updates job status and result when done.
3. Background job with notification
Use when you can push the result to the client.
- WebSockets, Server-Sent Events, or push notifications.
- When worker finishes, it notifies connected clients.
Summary
- Synchronous tasks run from start to finish while the caller waits. They are simpler and fit short, essential operations.
- Asynchronous tasks are started now but finish later, often in background workers. They improve responsiveness and scalability, especially for slow or heavy work.
- Understanding whether work is CPU‑bound or I/O‑bound helps you decide how to run it.
- Real backends mix both styles. You will choose per feature, focusing on user experience, reliability, and resource usage.
In the rest of the Background Processing section, you will learn about background jobs, message queues, and workers, which are the tools that bring asynchronous tasks to life in real backend systems.
Views: 10
KAHIBARO