Fix Wrong Times in Django Production: TIME_ZONE and USE_TZ

Fix Django showing times hours off in production: understand USE_TZ and UTC storage, set TIME_ZONE correctly, activate per-user time zones, render local times in templates, and stop cron and Celery schedules from drifting.

Problem statement

Order timestamps in the admin are two hours off. A report "for today" misses everything after 10 p.m. A Celery schedule set for 9:00 fires at 7:00. Locally everything looked right, because your laptop, your settings, and your expectations all shared one time zone. On the server, the operating system, the database session, Django, and the user's browser can each assume a different one.

Where times driftStored valueUTC when USE_TZDjango rendersTIME_ZONE or activeServer clockoften UTCReader seestheir local time?Store in UTC and convert only at the edges where people read or enter times.
  1. Keep USE_TZ = True. Django then stores aware datetimes in UTC.
  2. Set TIME_ZONE to your main audience, such as "Europe/Berlin". It controls display and form input, not storage.
  3. For users in several zones, activate their zone per request with django.utils.timezone.activate().
  4. Compute "today" with timezone.localdate(), not date.today() or datetime.now().
  5. Set Celery's CELERY_TIMEZONE and cron entries deliberately; servers usually run in UTC.

Step-by-step solution

1. Check the four clocks

timedatectl | grep "Time zone"
python manage.py shell -c "from django.conf import settings; from django.utils import timezone; print(settings.USE_TZ, settings.TIME_ZONE, timezone.now(), timezone.localtime())"
psql "$DATABASE_URL" -c "SHOW timezone;"

The server is often Etc/UTC. With USE_TZ = True, timezone.now() is UTC and timezone.localtime() converts it to TIME_ZONE. If localtime() shows the wrong hour, TIME_ZONE is wrong. If stored values look shifted, something wrote naive local times.

2. Store UTC, display local

USE_TZ = True
TIME_ZONE = "Europe/Berlin"

Django converts to UTC on save and back to the current time zone when templates and forms render values. In templates, values are converted automatically; in Python code you get UTC unless you call localtime(). Check that nothing in your code uses datetime.now() or datetime.utcnow(); both return naive values. Use timezone.now().

layers that handle timeBrowser and userreads local timeTemplates and formsconvert to active zonePython codeaware UTC from timezone.now()Database columntimestamptz, stored as UTCConversions happen only at the top two layersdjango-deployment.com
Bugs appear when code converts in the middle layers or mixes naive values in.

3. Activate per-user time zones

TIME_ZONE is one zone for everyone. If users span regions, store each user's zone and activate it in middleware:

import zoneinfo
from django.utils import timezone

class UserTimezoneMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        tz = getattr(getattr(request, "user", None), "timezone", None)
        if tz:
            timezone.activate(zoneinfo.ZoneInfo(tz))
        else:
            timezone.deactivate()
        return self.get_response(request)

Templates and forms then render and parse times in that user's zone.

4. Compute dates in the right zone

"Orders today" filtered with created__date=date.today() uses the server's date and compares it with the UTC date of each row. Use the local date and an aware range instead:

from datetime import datetime, time, timedelta
from django.utils import timezone

today = timezone.localdate()
start = timezone.make_aware(datetime.combine(today, time.min))
end = start + timedelta(days=1)
Order.objects.filter(created__gte=start, created__lt=end)

Lookups such as created__date use the current time zone when USE_TZ = True, but explicit ranges are clearer and use indexes.

5. Align schedules

Celery Beat interprets crontab schedules in CELERY_TIMEZONE, which defaults to UTC. Set it to the zone your schedule is written in:

CELERY_TIMEZONE = "Europe/Berlin"
CELERY_ENABLE_UTC = True

System cron uses the server's zone, usually UTC. Either write cron times in UTC or set CRON_TZ=Europe/Berlin at the top of the crontab where supported.

which setting is responsibleControlled byFixAdmin shows wrong hourTIME_ZONEset audience zoneToday misses late rowsdate.today()timezone.localdate()Beat fires off-hourCELERY_TIMEZONEmatch schedule zoneCron runs off-hourserver zoneUTC times or CRON_TZEach symptom has one owner; change that setting onlydjango-deployment.com
Changing the server clock to local time fixes none of these reliably.

Explanation

With USE_TZ = True, every datetime Django handles is aware, and PostgreSQL stores timestamp with time zone values as UTC instants. Nothing about storage depends on TIME_ZONE. That setting is only the default current time zone used when rendering and parsing. So a "wrong time" is almost always a rendering or computation problem, not corrupted data, which means it can be fixed without migrating rows.

Changing the server's system time zone to local time seems tempting but makes things worse: logs, cron, and backups shift twice a year with daylight saving time, and UTC-based tools no longer line up.

Edge cases / notes

  • USE_TZ = False projects. Old projects store naive local times. Switching on USE_TZ reinterprets existing rows as UTC; convert data deliberately first.
  • MySQL. Converting time zones in queries requires the MySQL time zone tables. See Fix MySQL Time Zone Definitions for Django.
  • Daylight saving gaps. make_aware can raise for times that do not exist in a zone. Validate user-entered times near DST changes.
  • APIs. Django REST Framework renders datetimes in the current zone with offsets. Clients should parse the offset rather than assume UTC.

Next steps

FAQ

Should TIME_ZONE be UTC?

It can be, if you display times through per-user activation or on the client. For a single-region site, setting your audience's zone makes the admin and emails readable.

Is my data wrong if the admin shows the wrong time?

Usually not. With USE_TZ = True the stored instants are correct UTC; only the display zone is wrong.

Why does the date lookup return different rows on the server?

date.today() uses the server clock, often UTC, while users think in local dates. Use timezone.localdate().

Should I change the server's system time zone?

No. Keep servers on UTC and handle zones in Django and Celery settings.

Keep going