TIMESTAMP Data Type in PostgreSQL

Learn the difference between PostgreSQL TIMESTAMP and TIMESTAMPTZ, how timezone-aware storage works, and how to use NOW(), CURRENT_TIMESTAMP, and timezone conversion functions.

6 min read · Back to overview

Quick Answer

PostgreSQL provides two timestamp types: TIMESTAMP (without timezone) stores date and time as-is, while TIMESTAMPTZ (with timezone) converts input to UTC for storage and back to the session timezone on retrieval. Use TIMESTAMPTZ for almost all production use cases.

Spin up a Postgres database in 20 seconds with Vela.

Try Vela Sandbox

PostgreSQL provides two timestamp data types: TIMESTAMP (without timezone) and TIMESTAMPTZ (with timezone). Both use 8 bytes of storage. The critical difference is how they handle timezone context when data is stored and retrieved.

Syntax

column_name TIMESTAMP               -- no timezone; stores value as-is
column_name TIMESTAMP WITH TIME ZONE -- alias for TIMESTAMPTZ
column_name TIMESTAMPTZ             -- recommended shorthand

TIMESTAMP vs. TIMESTAMPTZ

BehaviorTIMESTAMPTIMESTAMPTZ
StorageAs entered, no conversionConverted to UTC on write
RetrievalAs storedConverted to session timezone
Changes with server TZ changeNoYes
Recommended for productionRarelyAlmost always

Demonstration — insert the same value into both columns, then change the session timezone:

CREATE TABLE timestamp_demo (
  ts   TIMESTAMP,
  tstz TIMESTAMPTZ
);

SET timezone = 'America/Los_Angeles';

INSERT INTO timestamp_demo (ts, tstz)
VALUES ('2026-04-07 12:00:00-07', '2026-04-07 12:00:00-07');

SELECT ts, tstz FROM timestamp_demo;
          ts           |          tstz
-----------------------+------------------------
 2026-04-07 12:00:00   | 2026-04-07 12:00:00-07

Switch the session timezone to UTC and query again:

SET timezone = 'UTC';
SELECT ts, tstz FROM timestamp_demo;
          ts           |          tstz
-----------------------+------------------------
 2026-04-07 12:00:00   | 2026-04-07 19:00:00+00

ts (TIMESTAMP) never changes; tstz (TIMESTAMPTZ) adjusts to the new session timezone.

Practical Example

Track order lifecycle with TIMESTAMPTZ columns:

CREATE TABLE orders (
  id           SERIAL PRIMARY KEY,
  product_sku  VARCHAR(50)  NOT NULL,
  quantity     INTEGER      NOT NULL,
  placed_at    TIMESTAMPTZ  NOT NULL DEFAULT now(),
  confirmed_at TIMESTAMPTZ,
  shipped_at   TIMESTAMPTZ,
  updated_at   TIMESTAMPTZ  NOT NULL DEFAULT now()
);

INSERT INTO orders (product_sku, quantity)
VALUES ('SKU-101', 3)
RETURNING id, placed_at;

Getting the Current Timestamp

SELECT now();                -- 2026-04-07 10:00:00.123456+00
SELECT CURRENT_TIMESTAMP;   -- same as now()
SELECT CURRENT_TIME;        -- time only with offset
SELECT LOCALTIME;           -- local time without offset

Timezone Conversion

-- Convert a UTC timestamp to a specific timezone
SELECT '2026-04-07 19:00:00+00'::timestamptz
  AT TIME ZONE 'America/New_York';

-- Or using the timezone() function
SELECT timezone('America/New_York', '2026-04-07 19:00:00+00'::timestamptz);

Auto-Updating updated_at with a Trigger

PostgreSQL does not have an ON UPDATE equivalent, so use a BEFORE UPDATE trigger:

CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_orders_updated_at
BEFORE UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION set_updated_at();

Testing with Vela

Timezone-related bugs are notoriously hard to reproduce in development but appear immediately in production when servers are deployed across regions. With Vela’s database branching, you can test your timestamp-aware queries on a branch where the session timezone is set to match each of your target deployment regions, catching timezone edge cases before they reach production.

Production Tips

  • Use TIMESTAMPTZ (not TIMESTAMP) for all new columns — it correctly handles applications running across multiple timezones or deployed in different regions.
  • Set the database or session timezone consistently for all connections; inconsistent timezone settings are a common source of off-by-one-hour bugs.
  • Use now() or CURRENT_TIMESTAMP as defaults for created_at columns; use a BEFORE UPDATE trigger for updated_at since PostgreSQL has no built-in auto-update for timestamps.
  • When comparing timestamps across timezones, always use TIMESTAMPTZ literals with explicit offsets or AT TIME ZONE conversions.
  • Index TIMESTAMPTZ columns on high-volume tables to support efficient range queries — B-tree indexes are fully supported for timestamp range scans.

Continue in PostgreSQL Data Types: Interval.

Related in this section: Boolean · CHAR, VARCHAR, and TEXT · NUMERIC

Frequently Asked Questions

What is the difference between TIMESTAMP and TIMESTAMPTZ in PostgreSQL?
TIMESTAMP stores date and time without any timezone information — the value is stored exactly as entered and does not change when the server timezone changes. TIMESTAMPTZ stores timestamps as UTC internally; on retrieval, PostgreSQL converts the UTC value to the current session timezone. TIMESTAMPTZ is recommended for most applications.
Does inserting a TIMESTAMPTZ value lock the table?
No. INSERT and UPDATE on TIMESTAMPTZ columns acquire only row-level locks. The timezone conversion happens transparently on read and write without blocking other connections.
What happens to dependent triggers when I change a TIMESTAMP column to TIMESTAMPTZ?
Triggers that reference the column will continue to function, but any trigger function that casts the value to a specific timezone or compares it against a TIMESTAMP literal may behave differently after the type change. Test trigger logic on a branch before migrating.
Can I filter TIMESTAMPTZ columns with plain date literals?
Yes. PostgreSQL implicitly casts a date literal like '2026-04-07' to TIMESTAMPTZ (midnight at the start of the day in the session timezone), so WHERE placed_at >= '2026-04-01' works correctly. Be explicit about timezones in cross-timezone deployments.
What is the safest way to add an updated_at column to an existing production table?
Add the column with a DEFAULT: ALTER TABLE orders ADD COLUMN updated_at TIMESTAMPTZ DEFAULT now(). Then create a BEFORE UPDATE trigger to keep it current. Test the trigger on a Vela branch before deploying to confirm it does not create unexpected write amplification.