How to Convert Timestamps to Local Timezone in PostgreSQL

If your report groups events by date using a UTC timestamp, events that happen at 11pm local time silently land on the wrong day - because 11pm Eastern is already the next UTC date. The fix is to apply AT TIME ZONE before the date cast.

Data Engineerpythonpostgresqltimezone

The bug: grouping by UTC date instead of local date

Events are stored in UTC - which is correct. But when you cast directly to a date for grouping, PostgreSQL uses the UTC value:

-- Wrong: casts the UTC timestamp to a date
SELECT
    created_at::date AS event_date,
    COUNT(*) AS event_count
FROM events
GROUP BY created_at::date
ORDER BY event_date;

An event at 2024-03-04 23:45:00 America/New_York is stored as 2024-03-05 04:45:00 UTC. The ::date cast gives 2024-03-05 - a full day ahead of the user's local date. Events near midnight are the only ones affected, so the bug hides until someone checks closely.

The fix: AT TIME ZONE before the date cast

Convert to local time first, then cast to a date:

SELECT
    (created_at AT TIME ZONE 'America/New_York')::date AS event_date,
    COUNT(*) AS event_count
FROM events
GROUP BY (created_at AT TIME ZONE 'America/New_York')::date
ORDER BY event_date;

AT TIME ZONE shifts the timestamp to the named zone, then ::date extracts the local calendar date. The GROUP BY must use the same expression.

Parameterize the timezone

Hard-coding the timezone string works, but a better approach reads it from a variable so the same query works for any region:

import psycopg2, os

TIMEZONE = os.environ["REPORT_TIMEZONE"]  # e.g. "America/New_York"

query = """
    SELECT
        (created_at AT TIME ZONE %s)::date AS event_date,
        COUNT(*) AS event_count
    FROM events
    GROUP BY (created_at AT TIME ZONE %s)::date
    ORDER BY event_date;
"""

cur.execute(query, (TIMEZONE, TIMEZONE))
rows = cur.fetchall()

Pass the timezone name twice - once per %s placeholder. PostgreSQL's AT TIME ZONE accepts any IANA zone name (America/Chicago, Europe/London, Asia/Tokyo, etc.) and handles Daylight Saving Time transitions automatically.

How to verify the fix

Inspect raw rows to compare UTC vs local dates before and after:

SELECT
    id,
    created_at AS utc_time,
    created_at AT TIME ZONE 'America/New_York' AS eastern_time,
    created_at::date AS wrong_date,
    (created_at AT TIME ZONE 'America/New_York')::date AS correct_date
FROM events
ORDER BY created_at;

Any row where wrong_date != correct_date is an event that was mis-counted before the fix - typically events between midnight local and the UTC midnight that follows.

Store UTC, convert for display and aggregation

The rule: store timestamps in UTC everywhere. Convert to local time only at report or display time. This keeps the data portable across timezones and correct through DST changes. For Python-side conversion, zoneinfo (stdlib since 3.9) or pytz both accept the same IANA zone names PostgreSQL does.

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

Why do events near midnight show up on the wrong day in my report?

If your timestamps are stored in UTC and you group by date without converting timezone first, a 11pm Eastern event is already 4am the next UTC day. Cast to date after applying AT TIME ZONE so grouping uses the local calendar date, not UTC.

How do I convert a UTC timestamp to local time in PostgreSQL?

Use AT TIME ZONE with an IANA zone name: created_at AT TIME ZONE 'America/New_York'. This returns a timestamp in the target zone, handling DST automatically. Then cast to date if you need the local date.

Do I need to write the timezone name twice in the GROUP BY?

Yes - the GROUP BY expression must exactly match the SELECT expression. So if you write (created_at AT TIME ZONE %s)::date in the SELECT, repeat that same expression (with the same parameter) in the GROUP BY.

Keep learning

Make an ETL Pipeline IdempotentData projectSpeed Up a Slow SQL Report QueryData projectMake a CSV Importer Resilient to Bad DataData projectData roadmapStep by step to hiredData interview questionsSTAR answersAll Data projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →