Prevent overlapping runs of a periodic queue job

A periodic task scheduled every minute can overlap itself when one run takes longer than a minute. A durable lease can make the second run decline to start, but lease expiry also needs an ownership policy.
Periodic ticks meeting one active job lease.
Claim a pre-existing run row
This Django fragment assumes a PostgreSQL-backed RunLease row created for the job name. It has expires_at and token fields. Claiming the row is a short transaction.
from datetime import timedelta
from uuid import uuid4
from django.db import transaction
from django.utils import timezone
def acquire_lease(job_name):
with transaction.atomic():
lease = RunLease.objects.select_for_update().get(name=job_name)
now = timezone.now()
if lease.expires_at and lease.expires_at > now:
return None
token = str(uuid4())
lease.token = token
lease.expires_at = now + timedelta(minutes=5)
lease.save(update_fields=["token", "expires_at"])
return token
def release_lease(job_name, token):
return RunLease.objects.filter(
name=job_name, token=token
).update(expires_at=timezone.now())
The token prevents an old worker from releasing a lease that a newer worker already owns. The example duration is a policy choice, not evidence that the task always finishes within five minutes.
The lease row must exist before competing workers begin. Handling row creation races is another database rule; making name unique is part of the model contract.
Expiry is not cancellation
If a worker pauses for longer than the lease duration, another worker can acquire the lease while the first continues. The release token stops one cleanup error, but it does not stop the first worker from making stale writes or sending remote requests.
For database results, a generation or fencing value can be checked in every final update. For external actions, use the provider's idempotency and reconciliation mechanisms where available. If the work cannot be made safe under overlap, expiry alone is an insufficient recovery mechanism.
Test simultaneous acquisition on independent PostgreSQL connections, expiry, reacquisition and a late release from the old token. Also test a worker that outlives its lease. SQLite is not a substitute for this row-lock test.
Use a shared time authority or account for clock skew when workers compare expiry timestamps. Keep the claim transaction short and perform long work after releasing the row lock.
Postzyo provides related periodic-work context. PostgreSQL documents explicit locking, and Celery documents periodic-task overlap.