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.