MAD Study Hub

Home / MAD2 / Week 9

Week 9 · Asynchronous jobs: Redis and Celery

Official syllabus line: Asynchronous Jobs
About 90 minEnd Term material (see exam radar)BSCS2006 (+ project BSCS2006P) · Thejesh G N, Nitin Chandrachoodan
Before you start: MAD2 week 7 and 8 (Flask API, tokens), MAD1 Flask routes and SQLAlchemy, basic Python functions and decorators.

The one-minute story

Think of it like this

A restaurant with a waiter, an order board and a kitchen. You (the browser) tell the waiter (the Flask server) "one biryani". If the waiter walked into the kitchen, cooked for 30 minutes and only then came back, nobody else in the restaurant would be served. So the waiter writes the order on a slip, pins it on the order board (the message broker, here Redis) and comes straight back: "Order taken, your token number is 17." The cooks (Celery workers) take slips from the board whenever they are free and cook. When the food is ready the cook leaves the plate on the pickup counter (the result backend). You come back with token 17 and ask "Is it ready?" (polling). There is also a clock on the wall: every morning at 6 it automatically pins a slip "Make the daily tea" on the board (Celery beat).

Slow jobs do not belong inside a web request. They belong on a board, for cooks.

By the end of this week you can:

  • explain synchronous versus asynchronous work and why slow jobs must leave the request;
  • explain the roles of broker, worker and result backend, and what Redis does in this setup;
  • write a Celery task, call it with .delay() and .apply_async(), and predict what it returns;
  • configure Celery with Flask (app context) and start a worker and a beat scheduler;
  • design a CSV export flow (trigger, background build, status polling, download) and a daily reminder;
  • explain retries, idempotency and failure handling.

Words you need

Words you need
  • Synchronous - do the job and wait until it finishes before moving on.
  • Asynchronous - start the job and carry on; the result comes later.
  • Message broker - a middleman that receives messages from senders (producers) and holds them until receivers (consumers) take them.
  • Redis - a very fast in-memory data store, used here as the broker and result store (also as a cache).
  • Celery - a Python library for a task queue: it runs functions in separate worker processes.
  • Task - a Python function registered with Celery (@celery.task).
  • Worker - a process that fetches tasks from the broker and runs them.
  • Result backend - where task states and return values are stored.
  • Producer - the code that sends a task (your Flask app).
  • Polling - the client asks repeatedly "is it done yet?". Short polling asks on a timer.
  • Celery beat - the scheduler that sends periodic tasks to the broker at the right times.
  • Crontab - a schedule written with minute, hour, day fields, such as "every day at 18:00".
  • Idempotent task - running it twice has the same effect as once.
  • Webhook - the server calls a URL of the client when something happens (push, not poll).

Idea 1: Synchronous versus asynchronous, and why jobs leave the request

A Flask worker handles one request at a time (per worker process). A route that builds a PDF and sends an email takes, say, 12 seconds. During that time that worker is busy: other users queue up and the page feels dead, and the browser may time out. Using Celery does not make the job faster (it does not add CPU cores); it moves the job to another process so the web request can return in milliseconds.

# synchronous: the user waits 12 seconds, the server is blocked
@app.get("/report")
def report():
    pdf = build_pdf()          # 10 s
    send_email(pdf)            # 2 s
    return {"done": True}

# asynchronous: respond at once, work happens elsewhere
@app.get("/report")
def report():
    task = build_and_send.delay()
    return {"task_id": task.id}, 202      # 202 Accepted: work has been queued

Good fits for Celery: sending emails, generating reports and CSV/PDF files, image or video processing, calling slow external APIs, nightly cleanups, periodic emails or reminders. Bad fits: handling real-time user interactions, generating the page the user is waiting for, serving live notifications (that is for WebSockets or Server-Sent Events).

Do not use time.sleep() as a trick to wait for a job inside a route: it blocks the worker, which is the very problem we are solving.

Idea 2: Message broker concept

Think of 50 servers that must talk to each other. Point-to-point: every pair needs its own connection, so the number of links grows as O(n squared). With a central broker: each server connects only to the broker, so links grow as O(n). This counting question appears again and again; remember: point-to-point O(n^2), broker O(n).

Benefits of a broker: it decouples producers and consumers (they need not run at the same time, or know each other), makes the system scalable (add more workers), absorbs traffic spikes (messages wait in the queue until processed) and enables batch processing. If the consumer is slower than the producer, messages accumulate in the queue (they are not dropped, the producer does not block). A topic is a named channel to which messages are published. A broker does not let servers talk "directly without an intermediary", that is the opposite.

Two delivery styles: push queues (the broker pushes to consumers, good for real-time) and pull queues (consumers fetch when ready, good for batch work). Celery workers pull.

Idea 3: Redis, the board in the kitchen

The idea

Redis is an in-memory data structure store. It keeps data in RAM (so it is extremely fast, hundreds of thousands of operations per second), is not a relational database and does not use SQL. It is used as a database, cache and message broker, supports Pub/Sub, and can be a Celery broker and a Celery result backend.

It is more than strings. Typical data types and commands (I checked the behaviour with a Redis emulation):

TypeCommandsMeaning
StringSET name Asha, GET name, INCR visitsa value; numbers can be incremented atomically
ListLPUSH q a b, RPUSH q c, LRANGE q 0 -1, RPOP qordered; a natural queue
SetSADD s x x y, SMEMBERS sunique members: x stored once
HashHSET user:1 name A age 20, HGETALL user:1field-value map (like a small dict)
Sorted setZADD lb 10 a 5 b, ZRANGE lb 0 -1 WITHSCORESmembers ordered by score (leaderboards)
127.0.0.1:6379> SET name Asha
OK
127.0.0.1:6379> EXPIRE name 60
(integer) 1
127.0.0.1:6379> TTL name
(integer) 60            # seconds left; -1 = no expiry, -2 = key does not exist
127.0.0.1:6379> LPUSH q a b
(integer) 2
127.0.0.1:6379> LRANGE q 0 -1
1) "b"
2) "a"

LPUSH pushes to the left, so the last pushed (b) is first. A key with EXPIRE disappears by itself, which is how caches clean up. redis-cli opens the interactive shell; PING should answer PONG.

Persistence. Because data lives in memory, a restart would lose it unless persistence is on: RDB takes periodic snapshots to a file; AOF (append-only file) logs every write. Redis can use both. For Celery as a queue and cache, losing some data on restart is often acceptable; for your only copy of important data, keep it in the SQL database.

Python client: r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True), then r.set("k","v"), r.get("k"). Redis has numbered databases 0..15 inside one server; the URL redis://localhost:6379/1 means database 1. Redis also does not have to be the broker: Celery can use RabbitMQ and others, and a system can have several brokers.

Redis on Windows

Redis is built for Linux. On Windows: use WSL (install Ubuntu, then sudo apt install redis-server, sudo service redis-server start) or Docker (docker run -p 6379:6379 redis). Port 6379 is then reachable as localhost. Check with redis-cli ping.

Idea 4: Celery architecture and your first task

 Flask app           Redis (broker)         Celery worker(s)        Redis (backend)
 (producer)  --1--> [ task queue ] --2--> run task function --3--> [ state + result ]
     |                                                                 ^
     +---------------------- 4. AsyncResult(id).state / .get() ---------+
ComponentJob
Producer / Celery appyour Flask code that calls .delay(); creates a message
Brokerstores messages until a worker takes them (Redis, RabbitMQ)
Workerseparate process; imports your tasks, runs them, can run in parallel with other workers
Result backendstores state and return value (Redis, a database); optional but needed for .get() and status polling

In the real paper the code Celery('tasks', broker='redis://localhost:6379/1', backend='redis://localhost:6379/2') is explained like this: the broker stores task requests until a worker picks and executes them; the backend stores the result after execution. They can use the same Redis server in different databases; nothing forces different ports. The broker does not "track which worker processes which task".

# tasks.py
from celery import Celery
import time

celery = Celery("tasks",
                broker="redis://localhost:6379/0",
                backend="redis://localhost:6379/1")

@celery.task
def long_task():
    time.sleep(10)
    return "Task completed"

@celery.task
def add(x, y):
    return x + y

Start the worker (in its own terminal, from the folder with tasks.py), then call from Python or Flask:

celery -A tasks worker -l info          # for a Flask app: celery -A app.celery worker -l info
result = add.delay(4, 6)
print(result)            # a task id string such as 9c1f...: it is an AsyncResult, NOT 10
print(type(result))      # <class 'celery.result.AsyncResult'>
print(result.status)     # 'PENDING' right away (worker not finished or not started)
print(result.get(timeout=5))   # 10, waits up to 5 s for the worker
print(result.status)     # 'SUCCESS'
Solved example: 1. What does this print?
check = add.delay(4, 6)
print(check.status)              # (a)
print(check.get(timeout=5))      # (b)

(a) PENDING: .delay() returns immediately with an AsyncResult, the work has not finished. (b) 10 after the worker finishes. And for print(add.delay(4, 6)) you see a task id, never 10.

Calling long_task.delay() with workers running: the call returns at once; the task executes asynchronously and its result 'Task completed' is available after about 10 seconds. Tasks are asynchronous by default (not synchronous), can run on another machine than the Flask app, and need a running worker.

delay versus apply_async

task.delay(a, b) is a shortcut for task.apply_async(args=(a, b)). Use apply_async for options:

send_email.delay("a@x.com")                                  # send now
send_email.apply_async(args=["a@x.com"], countdown=10)       # run 10 s later
send_email.apply_async(args=["a@x.com"], eta=datetime_utc)   # run at a time
send_email.apply_async(args=["a@x.com"], expires=60)         # drop if not started in 60 s

With countdown=10 the message is sent to the queue immediately but the worker starts it only after the 10 seconds have passed (it does not wait 10 seconds before dispatching). .delay() dispatches immediately and does not wait for any user input.

Task states

StateMeaning
PENDINGnot started, or unknown id (also what you see for a task id that never existed)
STARTEDworker began (only recorded if task_track_started is on)
RETRYfailed and will be retried
SUCCESSfinished, .get() returns the value
FAILUREraised an exception; .get() re-raises it

Idea 5: Celery with Flask, and the app context

The worker is a separate process. Your task often needs the database or current_app, which only exist inside a Flask app context. The standard pattern (from the Flask docs) makes every task run inside one:

# celery_app.py
from celery import Celery, Task
from flask import Flask

def celery_init_app(app: Flask) -> Celery:
    class FlaskTask(Task):
        def __call__(self, *args, **kwargs):
            with app.app_context():
                return self.run(*args, **kwargs)

    celery = Celery(app.name, task_cls=FlaskTask)
    celery.config_from_object(app.config["CELERY"])
    celery.set_default()
    app.extensions["celery"] = celery
    return celery

# app.py
app = Flask(__name__)
app.config["CELERY"] = dict(
    broker_url="redis://localhost:6379/0",
    result_backend="redis://localhost:6379/1",
    timezone="Asia/Kolkata",
)
celery = celery_init_app(app)

I ran this pattern with eager mode: a task that returns current_app.name returned the app's name, proving the context works. Then start the worker with celery -A app.celery worker -l info (module app, the variable celery). Some course templates instead create the Celery object and then set celery.conf.update(...): check your course notes for the exact style. Rules:

  • The worker must be able to import your tasks (include=["tasks"] or put them in the module you pass to -A).
  • Restart the worker after you change task code; it loads code once.
  • Arguments and return values travel as JSON, so pass simple things: ids, strings, numbers, not SQLAlchemy objects (job_report.delay(user) is bad; pass user.id and load the user inside the task).
  • On Windows the default pool does not work well; start the worker with celery -A app.celery worker -l info -P solo. WSL or Docker avoids this.

Idea 6: Scheduled (beat) versus on-demand tasks

On-demandScheduled (periodic)
Triggered byyour code calling .delay() (a user clicked "export")the clock, via Celery beat
Exampleexport my data as CSVdaily reminder at 18:00, monthly report on the 1st
Processes neededRedis + workerRedis + worker + beat

Celery beat is only a scheduler. At the due time it puts a message in the broker and stops. A worker must be running to pick it up. Without a worker the morning digest is never sent. (This exact claim appeared as a question: "the worker is optional, beat handles execution" is false.) You start beat separately: celery -A app.celery beat -l info. Do not schedule with @app.task_periodic (no such thing); a fixed-interval email needs Celery beat, no external scheduler.

from celery.schedules import crontab

celery.conf.beat_schedule = {
    "daily-reminder": {
        "task": "tasks.send_reminders",
        "schedule": crontab(hour=18, minute=0),               # every day 18:00
    },
    "monthly-report": {
        "task": "tasks.monthly_report",
        "schedule": crontab(minute=0, hour=8, day_of_month=1),  # 1st of month, 08:00
    },
    "every-10-min": {
        "task": "tasks.send_email",
        "schedule": 600.0,                                    # seconds
        "args": ("admin@x.com",),
    },
}

I printed these schedules: crontab(hour=18, minute=30) shows as 30 18 * * *; crontab(minute='*/10') is every 10 minutes; crontab(hour=7, minute=0, day_of_week='mon') is Mondays at 07:00. The task name is the module path plus function name (tasks.send_reminders). Set timezone so "18:00" means India time.

Your turn: 1. Which processes do I start?

Your app has an "export CSV" button and a daily 6 pm reminder. List what must be running.

Need a hint?

Broker, worker, scheduler, web app.

Show the full solution

Redis (broker and backend), the Flask app, at least one Celery worker, and Celery beat (for the reminder). The export needs only the worker; the daily reminder needs beat to enqueue and the worker to run.

Idea 7: The async CSV export flow

Problem: exporting all of a user's data takes 20 to 40 seconds. A fetch('/static/data.csv') after a fixed setTimeout of 41 seconds is a bad design: the job may take longer, and the user waits blindly. The right design is trigger, build, poll, download:

  1. Browser calls POST /api/export. Flask starts the task and returns 202 with a task id.
  2. The task builds the file in the background and returns its filename.
  3. Browser short-polls GET /api/export/<task_id> every few seconds until state is SUCCESS.
  4. Browser then fetches the download link.
# tasks.py
import csv, os
from celery_app import celery
from models import Item

@celery.task
def export_items(user_id):
    path = f"exports/items_{user_id}.csv"
    os.makedirs("exports", exist_ok=True)
    with open(path, "w", newline="") as f:
        w = csv.writer(f)
        w.writerow(["id", "name"])
        for it in Item.query.filter_by(user_id=user_id):
            w.writerow([it.id, it.name])
    return path
# routes
from celery.result import AsyncResult

@app.post("/api/export")
@jwt_required()
def start_export():
    task = export_items.delay(int(get_jwt_identity()))
    return {"task_id": task.id}, 202

@app.get("/api/export/<task_id>")
@jwt_required()
def export_status(task_id):
    res = AsyncResult(task_id, app=celery)
    if res.state == "SUCCESS":
        return {"state": "SUCCESS", "download": f"/api/download/{task_id}"}
    if res.state == "FAILURE":
        return {"state": "FAILURE"}, 500
    return {"state": res.state}          # PENDING / STARTED / RETRY
// Vue method: short polling
async startExport() {
  const r = await fetch('/api/export', { method: 'POST', headers: this.authHeader })
  const { task_id } = await r.json()
  const timer = setInterval(async () => {
    const s = await (await fetch('/api/export/' + task_id, { headers: this.authHeader })).json()
    if (s.state === 'SUCCESS') { clearInterval(timer); this.downloadUrl = s.download }
    if (s.state === 'FAILURE') { clearInterval(timer); this.error = 'Export failed' }
  }, 3000)
}

Short polling = asking on a fixed timer; it is the way to learn the state of an async task and trigger an action when done. Long polling = the server holds the request open until something happens, giving near real-time updates over plain HTTP (so "long polling cannot be done over HTTP" is false). A webhook is not short polling: the server pushes a message to a URL when an event happens. Server-Sent Events (SSE) keep one connection open for the server to push to the browser (one-way; Flask-SSE uses Redis as a shared channel).

Solved example: 2. One big task or many small tasks?

Generate 1000 reports.

Approach 1: for user in users: job_report.delay(user.id) creates 1000 small tasks. Approach 2: one task job_report() loops over all users.

With more than one worker, approach 1 spreads work across workers and finishes sooner. With one worker, both take about the same time (a single worker runs them one after another; approach 1 only adds queue overhead). Likewise for sending bulk emails: one send_bulk_emails task is slower than many send_email(email) tasks when more than 2 workers exist, and about equal with a single worker. Many small tasks also fail independently: one bad email does not stop the others.

Idea 8: Retries, idempotency, failure handling, monitoring

@celery.task(bind=True, autoretry_for=(ConnectionError,), retry_backoff=True, max_retries=3)
def send_mail(self, to):
    smtp_send(to)

# manual style
@celery.task(bind=True, max_retries=3)
def send_mail2(self, to):
    try:
        smtp_send(to)
    except ConnectionError as exc:
        raise self.retry(exc=exc, countdown=30)    # try again in 30 s
  • Retries: transient failures (network, SMTP) are retried a limited number of times. After max_retries the state becomes FAILURE.
  • Idempotency: a message may be delivered twice (worker crash, retry). Design tasks so repeating them is harmless: check "already sent?" before sending, use INSERT ... ON CONFLICT or unique keys, write output to a file named by task or user id so a rerun overwrites the same file instead of duplicating.
  • Failure handling: log exceptions, set a FAILURE state, show "Export failed, try again" in the UI; never let the polling loop run forever (set a maximum number of polls).
  • Pass ids not objects, and re-read the database inside the task so you work on fresh data.
  • Monitoring: Flower is a web dashboard for Celery (celery -A app.celery flower) showing workers, tasks and states. celery -A app.celery inspect active lists running tasks.
  • Queue growth: if tasks arrive faster than workers finish, messages accumulate in the queue; add workers (-c 4 concurrency, or more worker machines).
Watch out - real exam trap
  • .delay() returns an AsyncResult (prints a task id), not the function's return value.
  • .status right after .delay() is PENDING.
  • apply_async(countdown=10) dispatches immediately and runs 10 s later.
  • Beat alone does not run tasks: a worker is needed.
  • Celery does not need Redis: other brokers work; and Redis is not a relational database (no SQL).
  • Passing a SQLAlchemy object as a task argument; use ids.
  • time.sleep() in a route is not asynchronous; it blocks.
In your project

Your project almost certainly needs a daily reminder and a monthly report, plus an export job. In the viva: "Show me the beat schedule", "What happens if the worker is down?" (messages wait in Redis until a worker starts), "Why is export a task?" (it takes long, the request would block), "How does the front end know it finished?" (polling a status endpoint). Keep a one-line start-up recipe: Redis, flask run, worker, beat.

Your turn: 2. Predict the behaviour

A route calls report.delay() and then immediately returns {"ok": true}. The Celery worker is not running. What happens to the HTTP response and to the task?

Need a hint?

Where is the message stored meanwhile?

Show the full solution

The HTTP response still returns at once, because .delay() only puts a message in Redis. The task sits PENDING in the queue. When a worker starts later, it picks the message up and runs it.

Your turn: 3. Write the schedule

Write a beat entry that runs tasks.weekly_digest every Monday at 07:30.

Need a hint?

Use crontab with day_of_week.

Show the full solution
celery.conf.beat_schedule = {
    "weekly-digest": {
        "task": "tasks.weekly_digest",
        "schedule": crontab(hour=7, minute=30, day_of_week="mon"),
    }
}
Your turn: 4. Redis commands

Predict the output: RPUSH q a b c, then LPUSH q z, then LRANGE q 0 -1, then SADD s 1 1 2, then SCARD s.

Need a hint?

LPUSH adds at the head; sets keep unique members.

Show the full solution

LRANGE gives z a b c; SCARD s gives 2 (the member 1 was stored once).

Exam radar

Exam radar

About 39 distinct questions touch this week's keywords, almost all from the End Term (roughly 114 of 1471 End Term keyword matches, but only about 3 of 608 in Quiz 2, so this week is an End Term topic; counts are approximate and this week has fewer matched questions than week 7). The questions are mostly concept MCQ/MSQ, few code questions. They ask:

  • Celery basics: it is a task queue, runs tasks asynchronously, needs a broker and workers, can schedule tasks; Redis is not the only broker; a system may have multiple brokers;
  • delay versus apply_async(countdown=...), what .delay() returns, PENDING then the value (a 2026 paper);
  • broker versus backend in Celery(..., broker=..., backend=...); the role of Redis in Celery (store scheduled tasks, act as broker);
  • Beat: scheduling send_email every 10 minutes; a 2026 question that beat needs a worker;
  • message broker theory: decoupling, scalability, O(n) versus O(n squared) connections, spike absorption, topics, slower consumer causes queue growth;
  • polling: short versus long polling, webhook is not polling; the CSV-file question (poll the task state instead of setTimeout); one big task versus many small tasks; why a slow Flask route blocks other users;
  • Redis facts: in-memory data structure store, cache and broker, pub/sub, not SQL, not strings only.
Watch out - real exam trap
  • "Redis is a relational database" and "Redis only stores strings": both false.
  • "Message broker lets servers talk directly" is false.
  • With one worker, one big task and many small tasks take about the same time.
  • Wrong complexity pair: broker links grow as O(n), point-to-point as O(n squared).
  • @app.task_periodic and @app.on_periodic do not exist.

Quick quiz

Quick quiz

What will long_task.delay() do, when long_task sleeps 10 seconds and then returns 'Task completed', and workers are running?

  • Block the caller for 10 seconds
  • Return 'Task completed' at once
  • Return immediately; the task runs in a worker and the result is available after about 10 seconds
  • Fail, because tasks cannot sleep
.delay() only enqueues a message and returns an AsyncResult.

You want a Celery task send_email to run every 10 minutes. How?

  • With @app.task_periodic
  • With @app.on_periodic
  • With Celery beat and a beat schedule
  • Celery cannot do it; use an external scheduler
Celery beat is the built-in scheduler.

Select all TRUE statements about Celery and Redis. (select all that apply)

  • A Celery system needs worker processes.
  • Redis can be a broker and a result backend.
  • Redis must be used as the broker.
  • Redis stores data with SQL.
Celery supports other brokers such as RabbitMQ, and Redis is a non-SQL in-memory store.

In Celery('tasks', broker='redis://localhost:6379/1', backend='redis://localhost:6379/2'), what is the difference between broker and backend?

  • The backend tracks which worker runs which task
  • The broker holds task requests until a worker takes them; the backend stores the result afterwards
  • They must run on different ports
  • The backend is needed only for email tasks
Broker is the queue, backend is the result store.

Which is a TRUE statement about send_email.apply_async(countdown=10)?

  • The message is sent after waiting 10 seconds
  • The message is dispatched immediately and the worker starts the task after the 10 seconds
  • It waits for user input
  • It runs synchronously
countdown delays the start of execution, not the dispatch.

A file is generated by a Celery task in 20 to 40 seconds. What is the best way for the page to fetch it?

  • Call setTimeout for 41 seconds, then fetch
  • Fetch the file in a tight loop forever
  • Short-poll the task state every few seconds and fetch the file once it succeeds
  • Block the Flask route until the file exists
Polling the state is reliable; fixed timers guess.

Viva: say it out loud

Viva: say it out loud
Why did you use Celery in your project?

Some jobs, like exporting a CSV or sending monthly reports, take many seconds. If I did them inside the Flask request, the user would wait and the server would be blocked for others. With Celery the route queues the job and returns at once, and a worker does the job in the background.

Explain the roles of Redis, the worker and the backend.

My Flask app sends a task message to Redis, which acts as the broker and holds it in a queue. A Celery worker, which is a separate process, takes the message and runs the function. The result backend, also Redis in my setup, stores the state and return value, so the app can check the status later.

What is Celery beat and how is it different from a normal task?

Beat is a scheduler that puts periodic tasks, such as the daily reminder at 6 pm, into the broker at the right time using crontab schedules. A normal task is triggered by my code with .delay(). Beat does not run the task itself: a worker still has to be running.

How does the front end know that the CSV export is ready?

The export route returns a task id with status 202. The Vue app polls a status endpoint every few seconds, which uses AsyncResult(task_id).state. When it is SUCCESS, the response contains a download link.

What happens if the worker is down when a task is sent?

The message stays in the Redis queue and the task state stays PENDING. When a worker starts, it picks up the waiting messages and runs them. The web request itself was not affected.

What is idempotency and why does it matter for tasks?

An idempotent task gives the same result if it runs twice. Tasks can run twice after a retry or crash, so I check whether a reminder was already sent, or I overwrite the same export file, instead of creating duplicates.

What is Redis and what can it store?

Redis is an in-memory data structure store, very fast, used as a cache, a message broker and a database. It stores strings, lists, sets, hashes and sorted sets. It is not a SQL database. Data can be saved to disk with RDB snapshots or an append-only file.

How do you run Redis on Windows?

Redis is made for Linux, so I run it inside WSL or in Docker with docker run -p 6379:6379 redis. The Flask app and Celery then connect to localhost:6379.

Recap

Remember this
  • Slow work must leave the request: queue it, return 202 quickly, let a worker do it.
  • A broker decouples producers and consumers; point-to-point links grow as O(n squared), with a broker as O(n); queues grow if consumers are slow.
  • Redis is an in-memory data structure store (strings, lists, sets, hashes, sorted sets), usable as broker, result backend and cache; not SQL.
  • Celery: app (producer), broker, worker, result backend. @celery.task, .delay(), .apply_async(countdown=...), AsyncResult.
  • .delay() returns an AsyncResult at once; .status starts PENDING; .get() waits for the value.
  • Flask integration needs the app context inside tasks; start celery -A app.celery worker -l info (add -P solo on Windows).
  • Beat + crontab schedules periodic tasks, but a worker still runs them.
  • CSV export: trigger, build in a task, poll status, download. Make tasks idempotent, pass ids, retry transient errors, watch with Flower.
Code and facts to keep
  • celery = Celery("tasks", broker="redis://localhost:6379/0", backend="redis://localhost:6379/1")
  • @celery.task / @celery.task(bind=True, max_retries=3); raise self.retry(exc=exc, countdown=30)
  • t.delay(a, b) is t.apply_async(args=(a, b)); options countdown=, eta=, expires=
  • res = AsyncResult(task_id, app=celery); res.state (PENDING, STARTED, RETRY, SUCCESS, FAILURE); res.get(timeout=5)
  • celery -A app.celery worker -l info, celery -A app.celery beat -l info, celery -A app.celery flower
  • crontab(hour=18, minute=0); crontab(minute='*/10'); crontab(hour=8, minute=0, day_of_month=1); crontab(hour=7, minute=30, day_of_week='mon')
  • Redis: SET, GET, EXPIRE k 60, TTL k, INCR, LPUSH, RPUSH, LRANGE q 0 -1, SADD, SMEMBERS, HSET, HGETALL, ZADD, ZRANGE; redis-cli ping gives PONG
  • Windows: docker run -p 6379:6379 redis or Redis inside WSL
Connected weeks (learn them together)

Practise with real exam questions

These come straight from past papers (with the real figures and the official answer key). Open 60 real questions on this week   All End Term papers