2026年5月22日
2026年5月22日
PythonのCeleryでタスクキューを実装する方法(Redis・リトライ・定期実行・常駐化)
Celery を使うと、時間のかかる処理を「タスク」としてキューに積み、別プロセスのワーカーに順番に処理させられます。基本の構成は、ブローカーに Redis を使い、タスクを定義した Python モジュールを用意して、ワーカーを systemd で常駐させる形です。この記事では、最小構成から始めて、失敗時のリトライ、取りこぼしを減らす設定、Celery Beat による定期実行、本番での常駐化と監視までを順に説明します。
タスクキューが向いている処理
Web アプリや管理スクリプトの中で、すぐに結果を返す必要がない重い処理は、タスクキューに切り出すと扱いやすくなります。たとえば次のような処理です。
- メール送信や外部 API の呼び出し(相手の応答待ちで遅くなりやすい)
- 画像の変換、CSV の生成、レポートの集計
- 複数サイトの死活確認や、WordPress の REST API を通じた一括処理
単に「毎日3時に1回動かす」だけなら cron で足ります。Celery が役立つのは、処理の依頼が不定期に大量に来る、失敗したら間隔を空けて再試行したい、ワーカーを増やして並列に処理したい、といった場面です。
Celery を構成する要素
- プロデューサー:タスクをキューに入れる側(Web アプリやスクリプト)
- ブローカー:タスクを一時的に保管する仲介役。Redis か RabbitMQ がよく使われます
- ワーカー:キューからタスクを取り出して実行するプロセス
- 結果バックエンド:タスクの戻り値を保存する場所(結果が要らなければ省略できます)
- Beat:決まった時刻や間隔でタスクをキューに入れるスケジューラー
この記事では、ブローカーと結果バックエンドの両方に Redis を使います。
準備:Redis と Celery を入れる
Ubuntu や Debian の例です。ほかのディストリビューションではパッケージ名やサービス名(redis-server か redis か)が異なります。
sudo apt install redis-server
redis-cli ping # PONG と返れば起動している
python3 -m venv /opt/myjobs/venv
/opt/myjobs/venv/bin/pip install "celery[redis]" requests
/opt/myjobs/venv/bin/celery --version
Redis は外部からつながらないようにしておきます。設定ファイル(redis.conf)で bind 127.0.0.1 ::1 と protected-mode yes になっているかを確認し、同じサーバーにほかの利用者がいる場合はパスワード(requirepass や ACL)も設定します。インターネットに公開された Redis は、不正利用の標的になりやすいからです。
Celery は 2026年10月現在 5 系が使われています。なお、Windows は公式のサポート対象外とされているため、本番のワーカーは Linux で動かすのが無難です。
最小のタスクを作って動かす
作業ディレクトリ /opt/myjobs に tasks.py を作ります。
import os
import requests
from celery import Celery
from celery.schedules import crontab
from celery.utils.log import get_task_logger
app = Celery(
"tasks",
broker=os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379/0"),
backend=os.environ.get("CELERY_RESULT_BACKEND", "redis://localhost:6379/1"),
)
app.conf.update(
task_serializer="json",
accept_content=["json"],
result_serializer="json",
result_expires=3600, # 結果は1時間で消す
timezone="Asia/Tokyo",
task_acks_late=True, # 処理が終わってから完了を通知する
worker_prefetch_multiplier=1, # 1プロセスが先取りするタスクは1件まで
task_soft_time_limit=240, # 240秒で SoftTimeLimitExceeded を送る
task_time_limit=300, # 300秒で強制終了
)
logger = get_task_logger(__name__)
@app.task
def add(x, y):
return x + y
@app.task(
bind=True,
autoretry_for=(requests.RequestException,),
retry_backoff=True, # 1, 2, 4, 8...秒と間隔を延ばす
retry_backoff_max=600,
retry_jitter=True,
max_retries=5,
)
def check_url(self, url):
logger.info("checking %s (try %d)", url, self.request.retries + 1)
r = requests.get(url, timeout=10)
r.raise_for_status()
return r.status_code
別のターミナルでワーカーを起動します。
cd /opt/myjobs
./venv/bin/celery -A tasks worker --loglevel=INFO --concurrency=2
起動ログに登録済みタスク(tasks.add と tasks.check_url)が表示されれば準備完了です。Python の対話シェルからタスクを投入してみます。
from tasks import add, check_url
result = add.delay(2, 3)
print(result.id)
print(result.get(timeout=10)) # 5
check_url.apply_async(args=["https://example.com/"], countdown=30, expires=600)
delay() はすぐにキューへ入れる短縮形で、apply_async() は実行を遅らせる countdown や、一定時間を過ぎたら実行しない expires などを指定できます。ワーカー側のログに succeeded と出れば成功です。
リトライと冪等性
上の check_url は、通信エラーや HTTP エラー(requests の例外)が出ると、間隔を延ばしながら最大5回まで自動で再試行します。retry_jitter は待ち時間にばらつきを持たせ、多数のタスクが同じ瞬間に再送されて相手のサーバーに負荷が集中するのを避けるための設定です。
リトライを使うときに大事なのは、タスクを「同じ入力で何度実行されても結果が変わらない(冪等な)」作りにすることです。task_acks_late=True にすると、ワーカーが処理中に落ちてもタスクがキューに戻るので取りこぼしは減りますが、そのぶん同じタスクが2回実行されることがあります。メール送信や決済のように二重実行が困る処理は、送信済みの記録を確認してから実行するなどの工夫が要ります。
取りこぼしと詰まりを防ぐ設定
- **
worker_prefetch_multiplier=1**:ワーカーが先にまとめてタスクを確保しないようにし、長い処理と短い処理が混ざっても偏りにくくします。 - 時間制限:
task_soft_time_limitを過ぎるとタスク内にSoftTimeLimitExceeded例外が送られ、後片付けの機会が得られます。task_time_limitを過ぎると強制終了です。外部 API の応答が返らずワーカーが固まる事態を防げます。 - Redis の visibility_timeout:Redis をブローカーにした場合、完了通知のないタスクは一定時間(既定は1時間)たつと別のワーカーに再配送されます。1時間以上かかる処理や、
countdownで長く先送りするタスクがある場合は、broker_transport_options={"visibility_timeout": 秒数}で、最長の処理時間より長く設定します。
また、タスクの中で別タスクの result.get() を呼んで結果を待つ書き方は避けてください。ワーカー同士が互いを待って止まる原因になるため、Celery も既定でエラーにします。処理をつなげたいときは chain などの仕組みを使います。
Celery Beat で定期実行する
tasks.py に次を追加すると、10分おきにサイトの確認タスクがキューに入ります。
app.conf.beat_schedule = {
"check-site-every-10-min": {
"task": "tasks.check_url",
"schedule": crontab(minute="*/10"),
"args": ("https://example.com/",),
},
}
./venv/bin/celery -A tasks beat --loglevel=INFO
Beat はスケジュールに従ってタスクを「入れるだけ」で、実行するのはワーカーです。Beat を2つ以上起動すると同じタスクが重複して入るため、全体で1つだけ動かします。開発中はワーカーに -B を付けて一緒に起動することもできますが、本番では別プロセスに分けるのが一般的です。
systemd でワーカーを常駐させる
本番ではターミナルで起動したままにせず、systemd に任せます。専用ユーザー(ここでは celery)で動かし、root では動かさないでください。
/etc/systemd/system/celery-worker.service:
[Unit]
Description=Celery worker
After=network.target redis-server.service
[Service]
User=celery
Group=celery
WorkingDirectory=/opt/myjobs
EnvironmentFile=/etc/myjobs.env
ExecStart=/opt/myjobs/venv/bin/celery -A tasks worker --loglevel=INFO --concurrency=2
Restart=on-failure
TimeoutStopSec=300
[Install]
WantedBy=multi-user.target
/etc/myjobs.env にはブローカーの URL などを書き、パーミッションを 600 にします(パスワードを含む場合は redis://:your-password@localhost:6379/0 の形式)。Beat も同じ要領で celery -A tasks beat を実行するサービスを別に作ります。
sudo systemctl daemon-reload
sudo systemctl enable --now celery-worker.service
journalctl -u celery-worker.service -f
ワーカーは SIGTERM を受け取ると、実行中のタスクを終えてから停止します(ウォームシャットダウン)。TimeoutStopSec は、最長のタスクが終わるのに足りる時間にしておくと、デプロイ時の再起動で処理が途中で切られにくくなります。
動作状況の確認
./venv/bin/celery -A tasks inspect ping # ワーカーが応答するか
./venv/bin/celery -A tasks inspect active # 実行中のタスク
redis-cli llen celery # 既定キューに溜まっている件数
キューの件数が増え続けるなら、ワーカーの処理能力が足りていないか、ワーカーが止まっています。ブラウザで状況を見たい場合は Flower という監視ツールがありますが、管理画面を公開すると誰でもタスクの情報を見られてしまうため、認証を設定し、外部に直接公開しないようにします。
よくあるつまずき
- タスクが「未登録(unregistered)」と言われる:ワーカーが読み込んだモジュールにタスクが含まれていません。
-Aの指定と作業ディレクトリを確認し、タスクを変更したらワーカーを再起動します。 - タスクを入れても何も起きない:プロデューサーとワーカーで、ブローカーの URL(DB 番号を含む)が食い違っていることがよくあります。
- 結果が取れない:結果バックエンドを設定していないか、
result_expiresを過ぎて消えています。 - 同じタスクが2回動く:
task_acks_lateと visibility_timeout による再配送です。冪等な作りにするか、visibility_timeout を見直します。
まとめ
- Celery はブローカー(Redis など)・ワーカー・結果バックエンド・Beat で構成され、重い処理や不定期の大量処理をワーカーに任せられる
- Redis は外部に公開せず、シリアライザーは JSON に限定する
autoretry_forとretry_backoffで再試行し、タスクは何度実行されても困らない作りにするworker_prefetch_multiplier、時間制限、visibility_timeout で、取りこぼしと詰まりを防ぐ- Beat は1つだけ動かし、ワーカーとともに専用ユーザーの systemd サービスとして常駐させる