Планировщик задач в Python: APScheduler и его альтернативы
Когда time.sleep в цикле перестаёт справляться, встаёт вопрос выбора планировщика. Разбираем APScheduler, Celery beat, schedule и системный cron — что чем платит и когда какой подходит.

У вас есть задача, которая должна выполняться каждые пять минут: обновить кеш курсов валют, разослать напоминания, подчистить временные файлы. Первое решение — while True: do(); time.sleep(300) — работает до первого сбоя: процесс упал, и расписание встало. Дальше начинается выбор инструмента, и от него зависит, переживёт ли расписание перезапуск, масштабируется ли оно на несколько воркеров и что произойдёт, если задача займёт больше времени, чем интервал между запусками.
Разберём четыре подхода — APScheduler 3.x, Celery beat, библиотеку schedule и системный cron — с ценой каждого. Тема хранения и восстановления состояния джобов затрагивается только в контексте выбора; тонкости настройки брокера Celery — отдельный разговор.
Четыре подхода и что они решают
Планировщики в Python делятся по двум осям: где живёт расписание (в процессе или снаружи) и переживает ли оно перезапуск. От этого зависит всё остальное.
- schedule — расписание в памяти процесса, вы сами крутите цикл. Умирает вместе с процессом. Библиотека тонкая, зависимостей ноль.
- APScheduler — расписание в процессе, но может сохраняться в базу (job store). Есть фоновые режимы, не нужно вручную крутить цикл. Одна из немногих библиотек с триггером в стиле cron прямо в Python.
- Celery beat — отдельный процесс-планировщик, который кидает задачи в брокер (Redis, RabbitMQ), а выполняют их воркеры Celery. Расписание отделено от исполнения.
- системный cron — планировщик ОС запускает ваш скрипт как отдельный процесс. Python вообще не хранит расписание.
schedule: когда нужно просто и без базы
Библиотека schedule (версия 1.x) даёт читаемый синтаксис, но не запускает задачи сама — вы обязаны крутить цикл, обычно в отдельном потоке или в главном.
import schedule
import time
def update_rates():
print("Обновляю курсы")
# каждые 5 минут
schedule.every(5).minutes.do(update_rates)
# каждый день в 09:30
schedule.every().day.at("09:30").do(update_rates)
while True:
schedule.run_pending()
time.sleep(1) # проверяем расписание раз в секундуЧто вы платите: расписание живёт в памяти. Перезапустили процесс — всё, что не выполнилось, потеряно навсегда, без записи «пропущенный запуск». Нет пулов потоков из коробки: если update_rates висит 10 секунд, а рядом задача каждую секунду — она ждёт, потому что run_pending выполняет задачи последовательно в том же потоке цикла.
APScheduler: расписание с сохранением состояния
APScheduler 3.x умеет три триггера: interval (каждые N), cron (по крону) и date (один раз в момент). Планировщик работает в фоне и сам решает, когда запускать джобы, раскладывая их по пулу потоков или процессов.
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
def send_reminders():
print("Рассылаю напоминания")
scheduler = BackgroundScheduler()
# каждый будний день в 09:00
scheduler.add_job(
send_reminders,
CronTrigger(day_of_week="mon-fri", hour=9, minute=0),
id="reminders",
replace_existing=True,
)
scheduler.start()
# дальше основной код приложения продолжает работатьКлючевое отличие от schedule — job store. По умолчанию джобы хранятся в памяти, но можно подключить SQLAlchemyJobStore и указать URL базы. Тогда после перезапуска планировщик поднимет сохранённые джобы, а пропущенные во время простоя запуски обработает согласно параметрам misfire_grace_time и coalesce.
Главная ловушка APScheduler в фоновом режиме: если вы запускаете два экземпляра приложения с общим job store, оба планировщика попытаются выполнить одну джобу. APScheduler 3.x не координирует несколько планировщиков между собой — для этого нужна внешняя блокировка или переход на Celery beat.
Точные имена классов планировщиков (BackgroundScheduler, BlockingScheduler, AsyncIOScheduler) и параметры триггеров сверяйте с документацией вашей версии — API между 3.x и находящейся в разработке 4.x менялся.
Celery beat: расписание отдельно от исполнения
Celery beat — не самостоятельная библиотека, а компонент Celery. Процесс beat читает расписание и в нужный момент публикует задачу в брокер. Воркеры разбирают очередь и выполняют. Это единственный из четырёх вариантов, который честно масштабируется на несколько машин: beat один, воркеров сколько угодно.
# celery_app.py
from celery import Celery
from celery.schedules import crontab
app = Celery("tasks", broker="redis://localhost:6379/0")
@app.task
def cleanup_temp():
print("Чищу временные файлы")
app.conf.beat_schedule = {
"nightly-cleanup": {
"task": "celery_app.cleanup_temp",
"schedule": crontab(hour=3, minute=0), # каждую ночь в 03:00
},
}Что вы платите: инфраструктура. Нужен брокер, отдельно запущенный процесс beat (celery -A celery_app beat) и хотя бы один воркер (celery -A celery_app worker). Для «раз в пять минут обновить кеш» это перебор. Для системы с сотнями задач, ретраями и распределением по воркерам — обоснованно.
Системный cron: когда Python не должен висеть в памяти
Если задача — это «раз в день прогнать скрипт», держать ради неё постоянный Python-процесс расточительно. Строка в crontab запустит интерпретатор, отработает и завершится.
# crontab -e
# каждый день в 02:30 запустить скрипт
30 2 * * * /usr/bin/python3 /opt/app/nightly.py >> /var/log/nightly.log 2>&1Что вы платите: cron ничего не знает про Python. Виртуальное окружение подключаете сами (полный путь к интерпретатору venv), переменные окружения не наследуются из вашей сессии, а ошибки видны только если вы сами перенаправили вывод в лог. Зато состояние переживает что угодно, кроме сбоя самого сервера, и не ест память между запусками.
Сравнение
| Критерий | schedule | APScheduler | Celery beat | cron |
|---|---|---|---|---|
| Переживает перезапуск | нет | с job store — да | да | да |
| Расписание в коде на Python | да | да | да | нет |
| Cron-синтаксис | нет | да | да | да |
| Масштаб на несколько машин | нет | нет | да | нет |
| Внешние зависимости | нет | опц. база | брокер + воркер | только ОС |
| Держит процесс в памяти | да | да | да | нет |
Когда что выбирать
- schedule — задача внутри уже работающего процесса (бот, демон), потеря пропущенного запуска не критична, зависимостей хочется по минимуму.
- APScheduler — нужен cron-синтаксис в Python, важно восстановить расписание после перезапуска, но одна машина. Типичный случай — периодические задачи внутри веб-приложения на FastAPI или Flask.
- Celery beat — уже используете Celery для фоновых задач, нужны ретраи, несколько воркеров, распределение по машинам.
- cron — редкая задача (раз в день/час), которую нет смысла держать в памяти, и вы контролируете сервер.
Что ломается чаще всего
Три ошибки встречаются почти в каждом проекте с планировщиком.
- Наложение запусков. Задача выполняется дольше интервала. schedule дождётся её последовательно, APScheduler по умолчанию запустит вторую параллельно (пока не задан
max_instances=1), cron — стартует новый процесс поверх старого. Всегда решайте явно, что делать при перекрытии. - Часовой пояс. Cron-триггеры зависят от таймзоны процесса или сервера. Задача «в 09:00» может сработать в другое время после смены зоны или перевода часов. В APScheduler таймзону задавайте явно при создании триггера, а не полагайтесь на системную.
- Дубли при масштабировании. Развернули приложение с APScheduler или schedule в два реплики за балансировщиком — расписание отработает дважды. Оба инструмента внутрипроцессные и про соседей не знают. Если реплик больше одной, планировщик выносите наружу (Celery beat, cron на одной ноде или отдельный однопоточный сервис-планировщик).
Что настроить дальше
После выбора инструмента займитесь наблюдаемостью: планировщик, о падении которого вы узнаёте от пользователей, бесполезен. Логируйте каждый запуск с идентификатором джобы и временем, а для критичных задач добавьте внешний heartbeat-мониторинг — сервис, который шлёт алерт, если ожидаемый запуск не произошёл. Для APScheduler изучите события (EVENT_JOB_ERROR, EVENT_JOB_MISSED) и повесьте на них слушатель. Отдельно продумайте идемпотентность: любая задача по расписанию рано или поздно выполнится дважды, и код должен это переживать без последствий.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: APScheduler — официальная документация, Celery — Periodic Tasks (beat)
Частые вопросы
У меня APScheduler 4.x, а примеры для 3.x — они подойдут?
Задача по расписанию выполнилась дважды — что не так?
Можно ли использовать APScheduler внутри FastAPI или Flask?
Что произойдёт, если задача упадёт с исключением?
Cron запускает мой скрипт, но он не находит модули — почему?
Нужен ли брокер, чтобы просто выполнять задачу раз в час?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.