Celery Beat를 활용한 예약 및 주기적 작업
Celery Beat로 반복 작업을 실행하고 여러 작업자에서 크론 방식의 일정을 안전하게 조정합니다.
Celery Beat를 활용한 예약 및 주기적 작업은(는) CoddyKit의 무료 FastAPI Backend Development Bootcamp 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 FastAPI Backend Development Bootcamp 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. FastAPI Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
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 chosentimezone.
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-Bin 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 -Band 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_scheduleor viaadd_periodic_task; usetimedeltafor intervals andcrontab()for calendar logic. - Per-entry args/options parametrize tasks and set
queue/expiresto avoid stale pile-ups. - Timezones are UTC by default — pin
timezone+enable_utcexplicitly. - One clock only — never
worker -Bin production; run a single dedicated Beat replica. - Idempotency (Redis
SET NXlocks) protects against duplicate or redelivered runs. - RedBeat / django-celery-beat give editable, persistent, lock-protected schedules at runtime.
자주 묻는 질문
“Celery Beat를 활용한 예약 및 주기적 작업” 강의는 무료인가요?
네 — “Celery Beat를 활용한 예약 및 주기적 작업” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 FastAPI Backend Development Bootcamp 강의 전체를 잠금 해제할 수 있습니다. FastAPI Backend Development Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
“Celery Beat를 활용한 예약 및 주기적 작업”에서 뭘 배우나요?
Celery Beat로 반복 작업을 실행하고 여러 작업자에서 크론 방식의 일정을 안전하게 조정합니다. 브라우저에서 직접 실행하는 실습 코드로 FastAPI Backend Development Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
FastAPI Backend Development Bootcamp을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 FastAPI Backend Development Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“Celery Beat를 활용한 예약 및 주기적 작업” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 FastAPI Backend Development Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 FastAPI Backend Development Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- BackgroundTasks를 활용한 경량 오프로딩
- Celery 작업자를 FastAPI 앱에 연결
- 재시도, 멱등성 및 배달 불가 메시지 처리
- Celery Beat를 활용한 예약 및 주기적 작업