Background Tasks (Celery & RQ)
A background task moves slow work off the request thread, so your route returns instantly while a separate worker runs the heavy job and stores its result.
Learn Background Tasks (Celery & RQ) in our free Flask course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Flask course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
In this lesson you'll build an in-process job queue, mimic Celery's .delay() and result backend, and see the real Celery and RQ code you'd run in production.
When a user signs up, you might send a welcome email. Email is slow — an SMTP round-trip can take seconds. If you do it inside the request, the user waits and a worker is tied up. The fix: enqueue the slow job and return immediately, then let a separate worker run it.
The runnable example models this with a deque as the queue and a worker() that drains it. The route enqueues the email and responds accepted right away; the jobs only run when the worker is called.
Both signups return accepted immediately while two jobs sit pending. Only after worker() runs do the results appear — exactly how Celery and RQ separate enqueueing from execution.
Celery decorates a function so you can call task.delay(args) to enqueue it. That call returns immediately with a task id; the function does not run in your web process. The worker later writes the return value to a result backend you can poll by id.
The runnable example below recreates that shape: a @task decorator that adds a .delay() method, a queue, and a result_backend dict tracking each task's state from PENDING to SUCCESS .
RQ (Redis Queue) trades Celery's many features for a tiny, readable API. You create a Queue backed by Redis and call queue.enqueue(func, args) . It is ideal when you just need "run this function later" without scheduling, routing, or multiple brokers.
The runnable example keeps mirroring the concept — enqueue a function reference plus its arguments, then a worker pops and runs it — so you can see RQ's mental model before reading its real code.
Complete the queue below. Replace each ___ so a job is enqueued and a worker runs it.
❌ Task runs in the request, blocking the response
You called the function directly instead of task.delay(...) . Use .delay() (Celery) or q.enqueue(...) (RQ) so it goes on the broker and a worker runs it.
You tried to read res.result without setting backend= . A broker queues tasks; a separate result backend stores return values. Configure both if you need results.
Real task queues retry failed jobs. Build a worker that does too.
Lesson complete — your slow work runs off the request thread!
You built an in-process job queue, mimicked Celery's .delay() and a result backend, met RQ's simpler API, and saw the real production code for both.
🚀 Up next: Real-Time with WebSockets — push live updates to the browser instead of polling.
Practice quiz
Why move slow work to a background task?
- To make the database faster
- So the request returns immediately while the work runs elsewhere
- To avoid using Redis
- To skip error handling
Answer: So the request returns immediately while the work runs elsewhere. Offloading lets the request return fast while a worker runs the heavy job.
What is a broker in a task queue?
- A web server
- A template engine
- A message queue holding tasks until a worker picks them up
- A logging library
Answer: A message queue holding tasks until a worker picks them up. A broker (Redis/RabbitMQ) holds enqueued tasks for workers.
What does a result backend store?
- A task's return value to fetch later by id
- The broker connection
- The worker's source code
- Incoming HTTP requests
Answer: A task's return value to fetch later by id. The result backend stores each task's return value, keyed by task id.
What does calling task.delay(args) do in Celery?
- Runs the task inside the web process
- Blocks until the task finishes
- Deletes the task
- Enqueues it and returns an AsyncResult immediately
Answer: Enqueues it and returns an AsyncResult immediately. .delay() enqueues the task and returns immediately without running it in the web process.
How does RQ differ from Celery?
- RQ is simpler and uses only Redis
- RQ supports more brokers
- RQ has no worker
- RQ runs synchronously
Answer: RQ is simpler and uses only Redis. RQ trades Celery's many features for a tiny Redis-only API.
How do you enqueue a job with RQ?
- job.delay(...)
- queue.enqueue(func, *args)
- rq.run(func)
- func.background()
Answer: queue.enqueue(func, *args). RQ enqueues with queue.enqueue(func, *args).
How do you read a Celery task's result later?
- The web view returns it directly
- It prints to the console
- celery.AsyncResult(task_id).result
- delay() returns the value
Answer: celery.AsyncResult(task_id).result. Reload with celery.AsyncResult(task_id) and read .state and .result.
Which Celery states track a task's progress?
- OPEN, CLOSED, DONE
- QUEUED, RUNNING, DONE
- NEW, OLD, GONE
- PENDING, STARTED, SUCCESS, FAILURE
Answer: PENDING, STARTED, SUCCESS, FAILURE. Celery uses states like PENDING, STARTED, SUCCESS, and FAILURE.
Which HTTP status commonly signals 'queued for later'?
- 202 Accepted
- 404 Not Found
- 500 Internal Server Error
- 301 Moved Permanently
Answer: 202 Accepted. A 202 Accepted response means the work was queued and will run later.
When is RQ the better choice over Celery?
- For multiple-broker routing
- For complex scheduling pipelines
- For straightforward job queues without extra features
- When you cannot use Redis
Answer: For straightforward job queues without extra features. RQ fits simple 'run this later' job queues; Celery suits complex pipelines.