Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Postgres TIMESTAMPTZ: Why Is It Always UTC?

Learn why PostgreSQL stores TIMESTAMPTZ in UTC and how JVM or JDBC time zone settings affect retrieval in Spring Boot apps.
PostgreSQL TIMESTAMPTZ always returning UTC confusing developers using Spring Boot with split digital time display and PostgreSQL logo PostgreSQL TIMESTAMPTZ always returning UTC confusing developers using Spring Boot with split digital time display and PostgreSQL logo
  • 🕰️ PostgreSQL TIMESTAMPTZ stores timestamps in UTC. It takes the time from your session's time zone and changes it to UTC when you add it.
  • ⚙️ JDBC uses the JVM's default time zone. This can be different from the Postgres session, and this causes problems.
  • 🔄 Spring Boot does not automatically match JDBC and PostgreSQL session time zones. You must set them up yourself.
  • 🌐 TIMESTAMPTZ shows an exact point in time. It does not show how it was formatted or its first time zone.
  • 🔍 Set the time zone in JDBC and JVM clearly. This makes sure time works the same everywhere.

Postgres TIMESTAMPTZ: Why Is It Always UTC?

Time zone problems can cause bugs that are hard to find. These bugs can be very bad in live systems, leading to missed deadlines, mixed-up logs, or wrong data across systems. PostgreSQL’s TIMESTAMPTZ data type often causes these problems, especially when you use it with Spring Boot and JDBC. Many people have added a timestamp, expecting it to come back in their local time, but got UTC instead. You are not alone if this happened to you. Let's see how it all really works.


TIMESTAMP vs TIMESTAMPTZ in PostgreSQL

Handling dates and times the right way is very important in systems that run across many places or are used worldwide. PostgreSQL has two main types for timestamps: TIMESTAMP WITHOUT TIME ZONE and TIMESTAMP WITH TIME ZONE (also called TIMESTAMPTZ). If you know how they are different and how they work inside, you can stop bugs related to time.

What Does TIMESTAMP WITHOUT TIME ZONE Store?

TIMESTAMP WITHOUT TIME ZONE, often called just TIMESTAMP, saves a date and time without any time zone information. It saves the exact date and time you give it.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

SELECT TIMESTAMP '2023-11-01 12:00:00';
-- Returns 2023-11-01 12:00:00

This type looks simple. But it does not have any details about when or where this date and time happened. For example, someone in Japan and someone in New York could each type 2023-11-01 12:00:00. They might mean two very different times. But PostgreSQL treats them the same. The system saves the time exactly as you type it. It assumes all users see time the same way, but this is often not true.

What Does TIMESTAMPTZ (TIMESTAMP WITH TIME ZONE) Store?

The name might make you think TIMESTAMPTZ saves a time zone string with each timestamp. But this is not true. Instead, it makes sure that no matter what time zone you put the value in, PostgreSQL changes that time to UTC behind the scenes. And then it saves that UTC time.

SELECT TIMESTAMPTZ '2023-11-01 12:00:00+03';
-- Gets stored internally as: 2023-11-01 09:00:00 UTC

Then, when you ask for the data, PostgreSQL will change that UTC timestamp to your session's current time zone so you can see it.

For example, if your session's time zone is set to 'Asia/Kolkata', you might see:

2023-11-01 14:30:00+05:30

So, TIMESTAMPTZ knows about time zones. This means PostgreSQL keeps track of the exact moment in time using UTC as a standard reference point.


How Postgres TIMESTAMPTZ Works Inside

If you know how PostgreSQL works inside, you can see that this is how it should be. It is not a bug.

Step-by-Step Action: Add → Save → Get Back

  1. When you add a timestamp: When you put a timestamp into a TIMESTAMPTZ column, PostgreSQL looks at the session's time zone or the time zone in the string. Then it changes the value to UTC.

    INSERT INTO events (event_time) 
    VALUES ('2023-11-01 08:00:00+03');
    -- Stored as 2023-11-01 05:00:00 UTC
    
  2. How it saves the data: PostgreSQL keeps the timestamp in UTC. It uses 4–8 bytes, depending on the version, like the UNIX epoch format. It does not save the first time zone offset.

  3. When you get the data back: When you SELECT data, PostgreSQL changes the UTC time back to the current session's time zone.

    SET TIME ZONE 'Asia/Kolkata';
    SELECT event_time FROM events;
    -- Output: 2023-11-01 10:30:00+05:30
    

This split between how data looks and what the session means is very important. It helps systems that deal with global time, like for logs or coordination services.

You can see these settings clearly:

SHOW TIMEZONE;
SELECT now();

PostgreSQL shows the session's current time zone (SHOW TIMEZONE). And then it shows the current time (SELECT now()) changed to match.


Why It Shows UTC: Session Settings vs System Defaults

When you first use PostgreSQL, it uses UTC as its session time zone:

SHOW timezone;
-- returns UTC

If you or your app do not clearly set a different session time zone (through Spring Boot settings, JDBC, system variables, or SQL), Postgres will do everything in UTC. This means it saves and shows times in UTC.

So, what happens? Apps that expect to see local time get confused. They get UTC time instead.

This problem gets bigger when:

  • The JVM uses a time zone that is not UTC. (So what your app thinks is “now” is different.)
  • The JDBC driver does not tell PostgreSQL what time zone to expect.
  • The front-end or logs show times in local time.

Spring Boot + JDBC + Time Zone Behavior

Let's look at what happens when you use Spring Boot, JDBC, and PostgreSQL.

How JDBC Deals with Time Zones

Java's JDBC driver for PostgreSQL does not make you use a specific time zone when you connect. This is true unless you set it clearly. By default:

  • JDBC uses the JVM’s system time zone.
  • PostgreSQL uses UTC by default, unless you change it.
  • JDBC does not set the session time zone in PostgreSQL on its own.

So, if you do not set things up the right way, PostgreSQL and your JVM might read or show timestamps in different ways. This causes confusion and bugs that are hard to find.

JVM Time Zone Can Trick You

The JVM starts with the operating system's time zone when it boots up. You can change this in two ways:

  1. With a command:

    -Duser.timezone=Asia/Kolkata
    
  2. In your Spring Boot App's code:

    @PostConstruct
    void setup() {
        TimeZone.setDefault(TimeZone.getTimeZone("Asia/Kolkata"));
    }
    

But be careful: This only updates the JVM's time zone. It does not change your database (PostgreSQL) session’s current time zone.

So, if you do not set up the session clearly, all TIMESTAMPTZ values from PostgreSQL will still be in UTC. This is true even if your JVM is in Tokyo.


Common Mistake: Expecting the First Time Zone Back

Many developers make a common mistake here:

Imagine you add this timestamp in SQL:

INSERT INTO events (event_time) VALUES ('2024-04-01 10:00:00+05:30');

What happens in the database?

  • PostgreSQL removes the +05:30 offset. It changes the timestamp to UTC: '2024-04-01 04:30:00'.
  • The saved value is now in universal time (UTC). And the system does not keep any record of the first offset.

What do you get when you ask for it?

  • If your session uses UTC: 2024-04-01 04:30:00+00:00
  • If your session uses Asia/Kolkata: 2024-04-01 10:00:00+05:30

This is not a bug. It works as PostgreSQL officially says it should.

💡 Rule: TIMESTAMPTZ shows when something happened. It does not show where or how the timestamp was put in.


How to Match JDBC and PostgreSQL Time Zones Correctly

Do not leave things to chance. Make sure you set things clearly.

Set Postgres Session Time Zone with the JDBC URL

The PostgreSQL JDBC driver lets you send options directly to the database session:

jdbc:postgresql://localhost:5432/yourdb?options=-c%20timezone%3DAsia/Kolkata

This makes sure that the database session uses Asia/Kolkata right when it connects.

Set DB Time Zone Using Spring Boot

Here is a clearer way to do it in Spring Boot:

spring.datasource.hikari.connection-init-sql=SET TIME ZONE 'Asia/Kolkata';

This command runs for every new database connection. It makes sure that the session's time zone is set correctly.

Match the JVM Time

If system logs or items with dates in your app need to be in the same time zone:

@PostConstruct
public void init() {
    TimeZone.setDefault(TimeZone.getTimeZone("Asia/Kolkata"));
}

Or use this:

-Duser.timezone=Asia/Kolkata

Now, your JVM, Postgres session, and JDBC timestamps will all show the same time.


Good Ways to Handle Time Zones

It is very important for time to be the same, whether you are dealing with logs, audits, or business actions. Follow these good ways:

  • ✅ Always use TIMESTAMPTZ for events that matter in the real world.
  • ⚠️ Do not use TIMESTAMP WITHOUT TIME ZONE unless you are showing events that happen at the same local time again and again (for example, tea at 4 PM every day).
  • 📅 Change times to the user's local zone only when you show them in the UI or API.
  • 🌐 Always send timestamps in ISO 8601 format with zone offsets.
  • 🔍 Check your JVM, session, and database time zone settings in integration tests.

Finding UTC Problems in Your App

Timestamps are not matching? Here is a list of things to check:

  1. Check Postgres session time zone:

    SHOW TIMEZONE;
    SELECT current_setting('TimeZone');
    
  2. Look at the Spring Boot JDBC URL for any options=-c timezone= setting.

  3. Print the JVM Time Zone:

    System.out.println(TimeZone.getDefault());
    
  4. Compare timestamps you get from:

    • Java logs
    • API responses
    • psql CLI
      Make sure all of them show time in the same way.

Simple Spring Boot Example

Let's show how to handle time zones the right way.

JPA Model Using OffsetDateTime

@Entity
public class Event {
    @Id
    private Long id;

    private OffsetDateTime eventTime;
}

This makes sure your JSON output has time zone details.

Connection Settings

spring.datasource.hikari.connection-init-sql=SET TIME ZONE 'Asia/Kolkata';

Add from Postgres CLI (UTC):

INSERT INTO event (id, event_time) VALUES (1, '2023-06-01 06:00:00+00');

When you get this back in Java:

2023-06-01T11:30:00+05:30

What took place?

  • PostgreSQL saved UTC inside.
  • When you got it back, the session time zone (Asia/Kolkata) changed the output.
  • Java made it match correctly using OffsetDateTime.

How to Use Java's Time Classes the Right Way

The best set of tools for working with databases and APIs is:

  • 📦 Instant – always UTC, good for internal database use.
  • 🔁 OffsetDateTime – builds on Instant with an offset, good for APIs.
  • 🌍 ZonedDateTime – adds the time zone name, helpful for systems that run at certain times.

Example:

OffsetDateTime now = OffsetDateTime.now(ZoneId.of("Asia/Kolkata"));

It will show up as:

"2023-06-01T15:30:00+05:30"

This makes things clear. It does not matter if a person or a system reads the output.


When to Use TIMESTAMP WITHOUT TIME ZONE

TIMESTAMPTZ is usually the best choice. But sometimes, local time is a better fit, and you do not want it changed. Here are some examples:

  • 🧒 Birthdays: Someone was born at 12:01 AM local time, not UTC.
  • 🕐 Events that happen again: Daily tasks run at 2 PM every day, even with daylight saving time changes.
  • 🏫 School Years, Financial Years: These are often shown without UTC details.

For all other things, like logs, transactions, and API timestamps, use TIMESTAMPTZ.


JSON, APIs, and ISO 8601 Formatting

It is very important to keep things the same when you send timestamps.

Set up Jackson the right way:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

This makes sure the output looks like this:

"eventTime": "2023-04-01T08:00:00+05:30"

This offset is very important for other systems to understand when something took place.


Manage Time Zones from Start to Finish

How PostgreSQL TIMESTAMPTZ works makes sense, but only after you learn the rules. And it means you need to be careful with all parts of your system.

Final list to check:

  • ✅ Save real events using TIMESTAMPTZ.
  • ✅ Set the database session time zone when you first connect (using the JDBC URL or Hikari settings).
  • ✅ Make the JVM time zone match using user.timezone if you need to.
  • ✅ Show time in Java using OffsetDateTime.
  • ✅ Show dates correctly using ISO standards in all APIs.

If you do these things, you will stop painful debugging times caused by wrong time zones. And it will make it simpler to think about time-based rules.


Citations

PostgreSQL Global Development Group. (2023). Data Types – Date/Time Types. PostgreSQL Documentation. https://www.postgresql.org/docs/current/datatype-datetime.html

Oracle. (2022). JDBC Developer’s Guide. Oracle JDBC Driver Timezone Behavior. https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Timestamp.html

Oracle Java Platform. (2023). Java SE Documentation – TimeZone Class. https://docs.oracle.com/javase/8/docs/api/java/util/TimeZone.html

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading