FastAPI Backend Development Bootcamp · Lekcja

Zadania zaplanowane i cykliczne za pomocą Celery Beat

Uruchamiaj cykliczne zadania za pomocą Celery Beat i bezpiecznie koordynuj harmonogramy w stylu cron między wieloma workerami.

Lekcja 4 z 413 kroki

Zadania zaplanowane i cykliczne za pomocą Celery Beat to bezpłatna lekcja FastAPI Backend Development Bootcamp na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej FastAPI Backend Development Bootcamp, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs FastAPI Backend Development Bootcamp zawiera 4 lekcji w sumie.

Części tej lekcji nie zostały jeszcze przetłumaczone i są wyświetlane po angielsku.

Why Celery Beat?

Your FastAPI app fires off one-off background tasks with Celery just fine, but some work must run on a schedule: send a digest email every morning, expire abandoned carts every 10 minutes, recompute analytics nightly.

Celery Beat is a scheduler process. It does not execute tasks itself — it wakes up on a tick, decides which tasks are due, and pushes them onto the broker (Redis/RabbitMQ). Your normal celery worker processes pick them up and run them.

  • Beat = the clock that publishes due tasks.
  • Worker = the muscle that runs them.

This separation is the whole reason periodic jobs scale: one Beat, many workers.

Defining the schedule

Schedules live on the Celery app under conf.beat_schedule. Each entry maps a name to a dict with the task path, a schedule (seconds, timedelta, or a crontab), and optional args/kwargs.

The simplest schedule is a fixed interval. Below, the cleanup task runs every 30 seconds. The task path string must match the worker's registered name exactly.

from celery import Celery
from datetime import timedelta

app = Celery("jobs", broker="redis://localhost:6379/0")

@app.task(name="tasks.cleanup_sessions")
def cleanup_sessions():
    # delete expired sessions from the DB
    return "cleaned"

app.conf.beat_schedule = {
    "cleanup-every-30s": {
        "task": "tasks.cleanup_sessions",
        "schedule": timedelta(seconds=30),
    },
}

Cron-style schedules with crontab()

Fixed intervals are coarse. For real calendar logic — "every weekday at 07:30" — use celery.schedules.crontab. It mirrors Unix cron fields: minute, hour, day_of_week, day_of_month, month_of_year.

  • crontab(minute=0, hour=0) — midnight every day.
  • crontab(minute="*/15") — every 15 minutes.
  • crontab(hour=7, minute=30, day_of_week="1-5") — 07:30 Mon–Fri.

Unset fields default to * (every value), exactly like a crontab line.

from celery.schedules import crontab

app.conf.beat_schedule = {
    "morning-digest": {
        "task": "tasks.send_digest",
        "schedule": crontab(hour=7, minute=30, day_of_week="1-5"),
    },
    "quarter-hour-sync": {
        "task": "tasks.sync_inventory",
        "schedule": crontab(minute="*/15"),
    },
}

Passing args and options per entry

Each schedule entry can carry arguments and per-call options. Use args (positional) or kwargs (keyword) to parametrize the same task differently across entries. The options dict lets you route a periodic task to a specific queue, set expires, or override priority.

expires is important for periodic jobs: if Beat enqueues a task but workers are backed up, an expired message is discarded instead of running late and stacking up.

from celery.schedules import crontab

app.conf.beat_schedule = {
    "warm-cache-eu": {
        "task": "tasks.warm_cache",
        "schedule": crontab(minute="*/5"),
        "kwargs": {"region": "eu-west"},
        "options": {"queue": "cache", "expires": 120},
    },
    "warm-cache-us": {
        "task": "tasks.warm_cache",
        "schedule": crontab(minute="*/5"),
        "kwargs": {"region": "us-east"},
        "options": {"queue": "cache", "expires": 120},
    },
}

Registering schedules with a decorator

Instead of one giant beat_schedule dict, you can register entries near the tasks using the on_after_configure signal and app.add_periodic_task. This keeps the schedule co-located with the code it triggers and avoids stale string paths.

add_periodic_task(schedule, signature, name=...) takes the interval or crontab first, then the task signature. You can pass task.s(arg) to bake in arguments.

from celery import Celery
from celery.schedules import crontab

app = Celery("jobs", broker="redis://localhost:6379/0")

@app.task
def rotate_logs(target):
    return f"rotated {target}"

@app.on_after_configure.connect
def setup_periodic_tasks(sender, **kwargs):
    sender.add_periodic_task(
        crontab(hour=3, minute=0),
        rotate_logs.s("app.log"),
        name="nightly-log-rotation",
    )

Timezones: the #1 schedule bug

By default Celery interprets crontab times in UTC. If you write crontab(hour=7) expecting local 07:00, your job will fire at the wrong wall-clock time. Always set the timezone explicitly and decide deliberately.

  • timezone — the zone crontab times are evaluated in.
  • enable_utc=True — keep internal timestamps in UTC (recommended) while still evaluating crontabs in your chosen timezone.

Pin the timezone in config so every developer and every server agrees, regardless of the host's local TZ.

app.conf.update(
    timezone="Europe/Istanbul",
    enable_utc=True,
)

# crontab(hour=9, minute=0) now means 09:00 Europe/Istanbul,
# stored/transmitted internally as UTC.

Running Beat and the persistent schedule file

You start the scheduler as its own process. The default scheduler stores its "last run" state in a small file so it does not re-fire everything after a restart.

  • celery -A app beat -l info — run Beat.
  • --schedule /var/run/celerybeat-schedule — where the shelve state file lives.
  • You can run worker + beat together in dev with celery -A app worker -B, but never use -B in production — it ties the clock to one worker's lifecycle.

In production, run exactly one Beat process. Two Beats = every periodic task fires twice.

The single-scheduler rule across workers

You can scale workers horizontally to dozens of pods and Celery handles it: the broker distributes each queued message to exactly one worker. But Beat is the clock, and you must run only one clock.

If two Beat processes run, each independently decides a task is due and publishes it, so subscribers see duplicate executions. Common ways this happens by accident:

  • Two replicas of a pod that both launch beat.
  • Using worker -B and then scaling that worker deployment to 2+.

Fix: a dedicated Beat Deployment with replicas: 1, separate from the worker Deployment you scale freely.

Idempotency: design tasks to survive duplicates

Even with one Beat, duplicates happen — a Beat restart at the wrong second, an at-least-once broker redelivery, or an operator mistake. The robust defense is making periodic tasks idempotent: running twice has the same effect as running once.

A simple pattern is a short-lived distributed lock in Redis using SET NX with an expiry. Whoever acquires the lock does the work; concurrent or duplicate runs skip safely.

import redis

r = redis.Redis(host="localhost", port=6379, db=0)

@app.task(name="tasks.charge_subscriptions")
def charge_subscriptions():
    # acquire a lock valid for 300s; only one runner proceeds
    got = r.set("lock:charge_subscriptions", "1", nx=True, ex=300)
    if not got:
        return "skipped: already running"
    try:
        # ... perform the billing run exactly once ...
        return "charged"
    finally:
        r.delete("lock:charge_subscriptions")

Database-backed schedules with django-celery-beat / RedBeat

The default file scheduler means changing a schedule requires editing code and restarting Beat. For dynamic schedules you want a backend store you can edit at runtime.

  • RedBeat — stores schedules in Redis; great for a Redis-only FastAPI stack. Set beat_scheduler = "redbeat.RedBeatScheduler".
  • django-celery-beat — stores entries in SQL tables, editable via admin UI.

RedBeat also provides a Redis-based lock so that if you accidentally start two Beats, only one is the active scheduler — a safety net for the single-clock rule.

app.conf.update(
    redbeat_redis_url="redis://localhost:6379/1",
    beat_scheduler="redbeat.RedBeatScheduler",
    beat_max_loop_interval=5,
)

A self-contained cron-due check

To build intuition for what Beat does on each tick, here is a tiny standalone simulation: given a list of cron-like jobs (interval in seconds and a last-run timestamp), decide which are due now. This is the core loop Beat performs — compare "now" against "next due" — without any broker.

def due_jobs(now, jobs):
    fired = []
    for name, interval, last_run in jobs:
        if now - last_run >= interval:
            fired.append(name)
    return fired

jobs = [
    ("cleanup", 30, 0),
    ("digest", 3600, 3500),
    ("sync", 900, 100),
]

now = 1000
print(due_jobs(now, jobs))  # ['cleanup', 'sync']

Quick Check: avoiding duplicate periodic runs

You deploy a FastAPI + Celery stack to Kubernetes. To handle load you scale your worker Deployment to 4 replicas, and each worker is started with celery -A app worker -B. Periodic tasks start running 4 times each. What is the correct fix?

Recap

You can now schedule recurring work with Celery Beat:

  • Beat schedules, workers execute — Beat publishes due tasks to the broker; one Beat, many workers.
  • Schedules live in beat_schedule or via add_periodic_task; use timedelta for intervals and crontab() for calendar logic.
  • Per-entry args/options parametrize tasks and set queue/expires to avoid stale pile-ups.
  • Timezones are UTC by default — pin timezone + enable_utc explicitly.
  • One clock only — never worker -B in production; run a single dedicated Beat replica.
  • Idempotency (Redis SET NX locks) protects against duplicate or redelivered runs.
  • RedBeat / django-celery-beat give editable, persistent, lock-protected schedules at runtime.
Bezpłatny start

Ucz się FastAPI Backend Development Bootcamp dzięki korepetycjom AI — za darmo

Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.

Kursy
21
Lekcje
84

Często zadawane pytania

Czy lekcja „Zadania zaplanowane i cykliczne za pomocą Celery Beat” jest bezpłatna?

Tak — pełny tekst „Zadania zaplanowane i cykliczne za pomocą Celery Beat” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu FastAPI Backend Development Bootcamp, przejdź na CoddyKit PRO. Kurs FastAPI Backend Development Bootcamp zawiera 4 lekcji w sumie.

Co nauczysz się w „Zadania zaplanowane i cykliczne za pomocą Celery Beat”?

Uruchamiaj cykliczne zadania za pomocą Celery Beat i bezpiecznie koordynuj harmonogramy w stylu cron między wieloma workerami. Ćwiczysz FastAPI Backend Development Bootcamp z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć FastAPI Backend Development Bootcamp?

Nie wymagamy żadnego doświadczenia. FastAPI Backend Development Bootcamp w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Zadania zaplanowane i cykliczne za pomocą Celery Beat”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji FastAPI Backend Development Bootcamp?

Tak. Każda lekcja FastAPI Backend Development Bootcamp zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Lekkie odciążanie za pomocą BackgroundTasks
  2. Podłączanie workerów Celery do aplikacji FastAPI
  3. Ponowienia, idempotencja i obsługa niedostarczonych komunikatów
  4. Zadania zaplanowane i cykliczne za pomocą Celery Beat
← Powrót do FastAPI Backend Development Bootcamp