Django APScheduler
django-apscheduler is a Django application that provides a lightweight wrapper around APScheduler, enabling the scheduling and persistence of background jobs within a Django project using its ORM. It allows for managing scheduled tasks directly through the Django admin interface and is suitable for applications requiring basic scheduling features without external task queues like Celery. The current version is 0.7.0, and releases occur as needed to support newer Django/Python versions and address fixes.
Common errors
-
APScheduler is running twice on production server! / Jobs running multiple times.
cause Multiple scheduler instances are being started, usually due to a multi-process web server (like Gunicorn with multiple workers) starting a scheduler in each worker process.fixEnsure only one scheduler instance is active. The recommended approach is to run the scheduler in a dedicated process using a custom Django management command (e.g., `python manage.py runapscheduler`), and manage this process with a supervisor (like systemd, Supervisor, or Circus). Avoid initializing the scheduler directly within `apps.py`'s `ready()` method if your web server uses multiple worker processes. [1, 7, 8, 13, 16] -
Lost connection to MySQL server during query / database connection failure / psycopg2.OperationalError: server closed the connection unexpectedly
cause Long-running scheduled jobs are holding onto database connections that time out, or the database server was restarted while jobs were running.fixDecorate all job functions that access the Django ORM with `@util.close_old_connections`. For environments where connections frequently time out, consider configuring Django's `CONN_MAX_AGE` setting and/or using a dedicated connection pooler (e.g., PgBouncer) with your database. Restart the `django-apscheduler` process after a database server restart. [1, 8, 13] -
ValueError: This Job cannot be serialized since the reference to its callable (<bound method xxxxxxxx. on_crn_field_submission of <__main__. xxxxxxx object at xxxxxxxxxxxxx>>) could not be determined. Consider giving a textual reference (module:function name) instead.
cause APScheduler cannot reliably serialize and deserialize lambda functions, bound methods, or nested functions when jobs are stored persistently.fixEnsure job functions are module-level functions or static/class methods that can be referenced by their `module:function_name` string. Avoid scheduling lambda functions, bound methods to specific instances, or functions nested within other functions. [5] -
django.db.utils.IntegrityError: UNIQUE constraint failed: django_apscheduler_djangojobexecution.job_id, django_apscheduler_djangojobexecution.run_time
cause Attempting to insert a duplicate job execution entry for the same job ID and run time, potentially due to multiple schedulers or race conditions.fixThis issue was mitigated by `unique_job_executions` constraint in v0.6.1 and further by a database lock in v0.7.0. Ensure you are on `django-apscheduler` 0.7.0+ and have run migrations. Also, rigorously follow the 'single scheduler' recommendation to prevent race conditions. [3]
Warnings
- breaking Version 0.7.0 dropped support for Python 3.8 and Django 3.2. Ensure your environment meets the new minimum requirements.
- gotcha Running `django-apscheduler` within a multi-process web server (e.g., Gunicorn with multiple workers) can lead to jobs running multiple times or being missed, as APScheduler lacks inter-process synchronization. It does not support shared job stores across multiple active schedulers.
- deprecated The `@register_job` decorator was deprecated in favor of APScheduler's native `add_job()` method or `@scheduled_job` decorator.
- gotcha Jobs that access the Django database (ORM) can suffer from 'lost connection' errors or timeouts. Django's database connection management is typically designed for short-lived HTTP requests.
- breaking APScheduler 4.0 introduced significant architectural changes (e.g., new job store design, event brokers, new terminology). `django-apscheduler` versions prior to one explicitly stating APScheduler 4.0 compatibility may not work correctly with `APScheduler` 4.x.
Install
-
pip install django-apscheduler
Imports
- DjangoJobStore
from django_apscheduler.jobstores import DjangoJobStore
- DjangoJobExecution
from django_apscheduler.models import DjangoJobExecution
- util
from django_apscheduler import util
- BlockingScheduler
from apscheduler.schedulers.blocking import BlockingScheduler
- BackgroundScheduler
from apscheduler.schedulers.background import BackgroundScheduler
- CronTrigger
from apscheduler.triggers.cron import CronTrigger
- IntervalTrigger
from apscheduler.triggers.interval import IntervalTrigger
Quickstart
import logging
from django.conf import settings
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
from django.core.management.base import BaseCommand
from django_apscheduler.jobstores import DjangoJobStore
from django_apscheduler.models import DjangoJobExecution
from django_apscheduler import util
logger = logging.getLogger(__name__)
def my_job():
# Your job processing logic here...
logger.info("My job is running!")
# The `close_old_connections` decorator ensures that database connections that have become
# unusable or are obsolete are closed before and after your job has run. You should use it
# to wrap any jobs that you schedule that access the Django database in any way.
@util.close_old_connections
def delete_old_job_executions(max_age=604_800):
"""This job deletes APScheduler job execution entries older than `max_age` from the database."""
logger.info(
"Deleting old job executions... (anything older than %s seconds)", max_age
)
DjangoJobExecution.objects.delete_old_job_executions(max_age)
class Command(BaseCommand):
help = "Runs APScheduler."
def handle(self, *args, **options):
scheduler = BlockingScheduler(timezone=settings.TIME_ZONE)
scheduler.add_jobstore(DjangoJobStore(), "default")
scheduler.add_job(
my_job,
trigger=CronTrigger(second="*/10"), # Every 10 seconds
id="my_job", # The `id` assigned to each job MUST be unique
max_instances=1,
replace_existing=True,
)
logger.info("Added job 'my_job'.")
scheduler.add_job(
delete_old_job_executions,
trigger=CronTrigger(day_of_week="mon", hour="00", minute="00"), # Midnight on Monday
id="delete_old_job_executions",
max_instances=1,
replace_existing=True,
)
logger.info(
"Added daily job: 'delete_old_job_executions'."
)
# Add a listener to log job executions and errors
# register_events(scheduler)
scheduler.start()
logger.info("Scheduler started. Press Ctrl+C to exit.")
# To run this, save it as `your_project_name/management/commands/runapscheduler.py`
# Then, in your Django settings.py, add 'django_apscheduler' to INSTALLED_APPS.
# Run migrations: `python manage.py migrate`
# Start the scheduler: `python manage.py runapscheduler` (preferably in a dedicated process)