
Why Your Django Admin Is an Hour Behind and Why Your Database Isn't Wrong
Why Django stores time in UTC, why that's right for production, and how to make the admin and Celery Beat show the time your users actually live in.
Why Your Django Admin Is an Hour Behind and Why Your Database Isn't Wrong
Short answer: Store time in UTC, and show local time only when a person needs to read it. In Django, USE_TZ = True takes care of storage. TIME_ZONE controls what people see.
It started with a phone call
I built the backend for an equipment-booking marketplace in Nigeria. The ops team ran everything from the Django admin. One afternoon the ops lead called:
"A farmer says he booked at 2pm. The system says 1pm. Is someone editing records?"
Nobody was. The farmer booked at 2pm Lagos time, and the database stored 13:00 UTC. Those are the same moment. The admin was just showing UTC, because settings had:
TIME_ZONE = "UTC"
USE_TZ = TrueThe config was correct from a developer's point of view, but to the person reading the screen it looked wrong.
Storage time vs display time
Think of directions in Lagos: "turn left after the yellow building opposite the mosque." That works if you know the area and means nothing to anyone else. GPS coordinates work for everyone.
UTC is the coordinates. Local time is "after the yellow building." You store the coordinates and give directions when someone asks.
USE_TZ = Truestores timezone-aware datetimes as absolute instants. Those are the coordinates.TIME_ZONEis only the default for display and input: the admin, forms, DRF output, and date lookups.
Why UTC is right for production
- It's unambiguous. "14:00" is a clock reading. A UTC timestamp is one exact moment.
- No daylight-saving surprises. Lagos has no DST, but London and Toronto do, and there some local times happen twice or not at all. UTC never jumps.
- Servers agree. Web, workers, and database can run in different regions and still share one clock.
- The rest of the ecosystem uses it. JWTs, Stripe, webhooks, and cloud logs all work in UTC.
The production bugs don't come from UTC. They come from naive datetimes and from showing UTC to people who think in local time.
The fix: change the display, not the data
TIME_ZONE = "Africa/Lagos"
USE_TZ = TrueThe admin now shows 2pm. No migration is needed, because the stored instants don't change. One thing to check: DRF now returns 2026-10-20T14:00:00+01:00 instead of ...13:00:00Z. It's the same moment, but make sure your frontend doesn't expect a trailing Z.
Surprise two: the 8am report that arrived at 9am
The client created Celery Beat schedules in the admin with django_celery_beat. They set hour=8 and meant Lagos time. Celery read it as UTC.
The catch is that every crontab row stores its own timezone, which defaults to CELERY_TIMEZONE at the time the row is created. Changing the setting later doesn't update existing rows.
CELERY_TIMEZONE = "Africa/Lagos"Then edit the existing crontabs in the admin, reset the last run times, and restart beat:
from django_celery_beat.models import PeriodicTask
PeriodicTask.objects.update(last_run_at=None)Interval schedules and countdown/eta tasks are not affected.
Two traps to watch
"Today" depends on whose today. created_at__date=... uses the current time zone. With UTC, a booking made at 00:30 in Lagos counts as yesterday.
Naive datetimes. Use timezone.now(), never datetime.now(). Treat Django's "received a naive datetime" warning as a bug.
Going beyond one country
Keep storing UTC. Let the mobile app format times in the device's zone, and activate each staff member's zone in middleware for the admin. Store IANA names like "Africa/Lagos", never "+01:00".
FAQ
Does changing TIME_ZONE need a migration? No. Only display, input, and date lookups change.
Is TIME_ZONE = "UTC" safe in production? Yes, for API-only backends. If people read the admin, set it to their zone.
The farmer was right, and so was the database. The screen was just showing the wrong clock.
Part of the Before You Scale series.
More posts
Docker Compose: Running a Real-World Application with Multiple Containers
Learn Docker Compose with Django, PostgreSQL, Redis, and Celery. Configure services, networking, volumes, and essential commands.
Docker Images, Containers and Dockerfiles: Understanding How Docker Works
Understand how Docker works under the hood. Learn about Docker images, containers, Dockerfiles, layers, ports, volumes, networks, and essential Docker commands.

