Scheduled tasks in Django, without running Celery
Django has no scheduler. The usual answer is Celery Beat, which means a broker, a worker and a beat process — three moving parts to run something once an hour.
What Django gives you
Nothing, by design. Django provides management commands and leaves scheduling to you. The common choices are system cron calling manage.py, or Celery Beat.
- Built-in scheduler
- None
- Celery Beat needs
- A broker, a worker process and a beat process
- System cron needs
- A host with crontab access
- Common failure
- The beat process dies and nothing reports it
Checked against Django docs on 18 August 2026. These change — if a deploy disagrees with this page, believe the deploy and tell us.
When this is the right answer
Most people reading this should keep what they have. It is worth being specific about when:
- You already run Celery for background jobs. Beat is a small addition to infrastructure you are already operating and monitoring, and adding a second scheduler would be worse.
- You have a server with cron and a handful of management commands. That setup is boring and reliable.
Where it runs out
Celery Beat is a lot of moving parts for a schedule
A broker to run, a worker to keep alive, and a beat process that must be a singleton. Run two beats by accident during a deploy and every task fires twice.
A dead beat process is silent
Nothing raises an exception when a scheduled task does not run. You find out from the absence of an effect, usually much later.
Platform-as-a-service often has no crontab
The simple option — a cron entry calling manage.py — is unavailable on exactly the hosting where you would most like something simple.
Scheduling it from outside
Put the management command behind a guarded view and call it on a schedule. No broker, no extra process, and the command stays a normal management command you can still run by hand.
views.py
from django.core.management import call_command
from django.http import JsonResponse, HttpResponse
from django.views.decorators.csrf import csrf_exempt
import hmac, os
@csrf_exempt
def run_task(request):
secret = request.headers.get('X-Cron-Secret', '')
if not hmac.compare_digest(secret, os.environ['CRON_SECRET']):
return HttpResponse('Unauthorized', status=401)
call_command('send_digests')
return JsonResponse({'ok': True})Worth saying plainly: this runs the command inside a web request, so it suits work measured in seconds, not minutes. If the job is genuinely long, keep Celery and let the scheduled call enqueue a task rather than do it.
What getNodi adds
getNodi calls a URL on a schedule and tells you what happened. Any schedule down to your plan’s floor, in your timezone rather than UTC, with retries, a record of every run — status, latency, response — and an alert the first time one fails. Credentials are encrypted at rest and requests can be HMAC-signed so your endpoint can verify they came from us.
The free plan runs 3 jobs hourly and needs no card, which is enough to find out whether this works for you. What it will not do is run a container, hold a connection open for an hour, or execute your code — it makes HTTP requests, and everything above is a way of making that enough.