Say you run a Django 5.x app and need a cleanup_stale_drafts task every night to purge week-old draft posts, a weekly report email on Mondays, an hourly Stripe reconciliation, and a daily content moderation pass over user uploads. Django ships zero scheduler out of the box. You have four credible ways to wire any of those: a Unix crontab calling python manage.py, an in-process scheduler like django-q2 or Celery Beat, a platform scheduler like Heroku Scheduler or Render Cron Jobs, or an external HTTP cron that hits a locked-down Django view.
This guide is the side-by-side walkthrough with code, plus when to pick which. For the broader Python picture, see the Python cron jobs guide.
Path 1: crontab calling python manage.py {custom_command}
The classic Django shape. Write a custom management command at myapp/management/commands/cleanup_stale_drafts.py, then fire it from cron.
# crontab -e
0 3 * * * cd /opt/myproject && /opt/myproject/.venv/bin/python /opt/myproject/manage.py cleanup_stale_drafts >> /var/log/cleanup_stale_drafts.log 2>&1Three things bite people here:
PATHis tiny under cron. Use the absolute path to the venv'spython, not barepython3.- No virtualenv autoload.
cdinto the project, or setDJANGO_SETTINGS_MODULE=myproject.settings.productionexplicitly in the crontab line. - Absolute path to
manage.py. Relative paths break when cron's working directory is/.
Pick this when: one VPS, one team, and you are fine grepping /var/log when something breaks at 3am.
Path 2: In-process schedulers
For teams already running a worker process, keep the schedule in Python.
- django-q2 is the actively maintained fork of django-q. Define schedules in the admin or in code, store them in the DB, and run a
qclusterprocess under supervisord or systemd. Decent retry semantics and the friendliest schedule UI of the three. - django-celery-beat is the answer when you already run Celery for async tasks. Beat stores schedules in the DB and dispatches to your existing workers. Cost: a broker (Redis or RabbitMQ), the beat process, plus the workers.
- APScheduler runs inside a single Django management command like
python manage.py runapscheduler. Light, no broker, but you must keep exactly one instance alive or jobs double-fire.
Tradeoff for all three: you pay for an always-on process whose only job is the clock. On Heroku that is a full extra dyno.
Path 3: Platform cron
If your Django app already lives on a managed platform, use its native scheduler.
- Heroku Scheduler runs every 10 minutes, hourly, or daily only, and fires
python manage.py cleanup_stale_draftsin a one-off dyno. No minute cadence, no cron syntax. - Render Cron Jobs support standard cron syntax down to the minute and run your Docker image with the command of your choice.
- AWS EventBridge fires a Lambda or ECS task that runs the management command. The wrinkle: cold starts, IAM, and packaging your Django app for Lambda (Zappa or a container image).
Path 4: External HTTP cron hitting a Django view
Expose a locked-down view and let an external cron service hit it. django.core.management.call_command runs your existing management command in-process, no subprocess.
import os
from django.core.management import call_command
from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
@csrf_exempt
@require_POST
def run_cleanup_stale_drafts(request):
expected = f"Bearer {os.environ['CRON_SECRET']}"
if request.headers.get("Authorization") != expected:
return HttpResponseForbidden()
call_command("cleanup_stale_drafts")
return HttpResponse(status=204)In Crontap, register POST https://api.yourapp.com/internal/cleanup-stale-drafts, set the Authorization: Bearer <secret> header, schedule 0 3 * * *, pick America/New_York. Pro is $3.25/mo annual flat for unlimited HTTP schedules at minute cadence on a 1-minute floor.
Schedule your Django jobs from outside the box. Free forever tier with one schedule. Try Crontap →
How to make a Django cron idempotent
Cron fires whether or not yesterday's run finished. The cleanest Django pattern is a transaction.atomic block with select_for_update on a single-row job lock table:
from django.db import transaction
from django.utils import timezone
from .models import JobLock
with transaction.atomic():
lock = JobLock.objects.select_for_update().get(name="cleanup_stale_drafts")
# do work here
lock.last_ran_at = timezone.now()
lock.save()If a second invocation arrives while the first is mid-flight, it blocks on the row until the first commits, then sees the fresh last_ran_at and no-ops. Alternative: a UniqueConstraint on (job_name, run_date) so a duplicate INSERT raises IntegrityError and you treat that as "already ran, skip."
FAQ
Should I use Celery Beat or django-q2?
Use Celery Beat if you already run Celery for background tasks. If you do not, django-q2 is much less infrastructure (one Redis or DB broker, one qcluster process) and the admin UI for managing schedules is friendlier.
How do I run a Django management command from a view?
Call django.core.management.call_command("your_command") from inside a POST handler. Authenticate the request with a bearer token (see Path 4) and use @require_POST + @csrf_exempt so external callers can hit it. It runs synchronously in the request process, so keep the command short or enqueue work for a django-q2 or Celery worker.
What about Django + Postgres pg_cron?
pg_cron is fine for pure-SQL maintenance, but you lose Django's ORM, signals, and middleware. Prefer it for VACUUM, partition rotation, or materialized view refreshes, not for application logic.
How do I avoid double-firing when scaling horizontally?
Path 1 and Path 2 both struggle here. Either elect a single "scheduler" instance (Celery Beat with --without-mingle, or one designated qcluster host) or move the schedule out of the app entirely with Path 3 or Path 4. With external HTTP cron the schedule fires once and your load balancer picks one instance to answer.
Related on Crontap
- Python cron jobs. The parent Python pillar.
- Cron troubleshooting hub. Debug failing schedules end to end.
- Cron job monitoring. Heartbeats and failure alerts for any HTTP target.
- Cron job not running?. The standard debug checklist.
- Scheduled AI jobs. Django + LLM moderation pipelines on a clock.
