PostgreSQL offers several meanings of “current time.” A transaction timestamp identifies the beginning of a transaction, a statement timestamp identifies a command’s arrival, and a wall-clock reading samples the current clock during evaluation. Choosing among them changes the meaning of audit fields, expiration checks, and observations made during long-running work.

A transaction has one start time

now(), transaction_timestamp(), and CURRENT_TIMESTAMP without a precision argument return the same transaction-start instant. They remain fixed throughout an explicit transaction, even if later statements run seconds or minutes afterward. CURRENT_TIMESTAMP can take a precision argument that rounds its result; that formatting precision is separate from when the time is sampled.

This stability is useful when related rows should share one logical transaction time. It does not record the exact moment each row was inserted, nor the eventual commit time. A transaction can wait for locks or perform substantial work before an insert, and it can still roll back. Calling a column created_at does not resolve which of these events its default actually represents.

In ordinary autocommit use, successive statements normally run in separate transactions, so now can appear to advance on every statement. That observation does not change the function’s contract. Introducing an explicit transaction later can expose an assumption that a supposedly live timestamp was actually fixed much earlier.

Statement markers and live readings

statement_timestamp() reports the receipt time of the latest command message from the client. It remains stable during that command’s execution and can differ from the transaction timestamp in later commands. Multiple SQL commands sent in one protocol message can share this marker; it is not necessarily the instant each internal operation starts after waiting.

clock_timestamp() reads the current wall clock when evaluated and can change within a single statement. Separate calls are not guaranteed to be distinct, because clocks have finite precision. The value also is not a monotonic timer: clock adjustments can affect differences between readings. Use a suitable monotonic clock for elapsed-duration guarantees rather than treating database wall time as an unconditionally increasing counter.

None of these values alone establishes commit order or a unique event identity across concurrent transactions. Choose a transaction marker for consistent related fields, a statement marker for command timing, or a wall-clock observation when that observation is what the application needs. Keep identity and ordering requirements explicit instead of expecting a timestamp to provide them automatically.

When it breaks

An expiration test using now inside a long transaction continues comparing against that transaction’s start. A condition that was true at the beginning can remain true after wall time has passed the deadline. Conversely, replacing every default with clock_timestamp can give related rows different values when they were meant to share a logical creation time. Decide which event the column represents before changing functions.

The special literal TIMESTAMPTZ 'now' is another trap in a column default. PostgreSQL converts it when the constant is parsed, so later inserts can keep receiving the table-creation time. DEFAULT now() is a function call evaluated when the default is used. Similar-looking spelling does not imply the same evaluation lifetime.

These functions return timestamptz values. Changing the session time zone changes how an instant is displayed, not the stored instant itself. Timestamptz does not preserve the originally supplied zone name. If the application must remember that original zone, model it separately rather than inferring it from a later display offset.

Check it quickly

Save this as timestamps.sql and run psql -X -v ON_ERROR_STOP=1 -f timestamps.sql in a disposable PostgreSQL 18 database. Keep psql’s normal autocommit behavior outside the explicit transaction. The second experiment needs separate insert transactions, and all tables are temporary.

\set ON_ERROR_STOP on
BEGIN;
SET LOCAL timezone='UTC';
CREATE TEMP TABLE moments (step integer,tx timestamptz,statement timestamptz,wall timestamptz);
INSERT INTO moments VALUES (1,now(),statement_timestamp(),clock_timestamp());
SELECT pg_sleep(0.03);
INSERT INTO moments VALUES (2,CURRENT_TIMESTAMP,statement_timestamp(),clock_timestamp());
SELECT step,tx,statement,wall FROM moments ORDER BY step;
SELECT min(tx)=max(tx) AS one_transaction_time,
       count(DISTINCT statement)=2 AS two_statement_times FROM moments;
SELECT now()=transaction_timestamp() AND now()=CURRENT_TIMESTAMP AS aliases;
SELECT extract(epoch FROM now()) AS instant_utc;
SET LOCAL timezone='Asia/Shanghai';
SELECT now() AS same_instant_different_display,extract(epoch FROM now()) AS instant_shanghai;
DO $$ BEGIN
  IF (SELECT min(tx)=max(tx) FROM moments) IS DISTINCT FROM true THEN
    RAISE EXCEPTION 'transaction time changed';
  END IF;
  IF now() IS DISTINCT FROM transaction_timestamp() THEN RAISE EXCEPTION 'aliases differ'; END IF;
  IF (SELECT tx FROM moments WHERE step=1) IS DISTINCT FROM now() THEN
    RAISE EXCEPTION 'time zone changed the instant';
  END IF;
END $$;
ROLLBACK;
CREATE TEMP TABLE defaults_demo (
  id integer,
  parsed timestamptz DEFAULT TIMESTAMPTZ 'now',
  called timestamptz DEFAULT now()
);
INSERT INTO defaults_demo (id) VALUES (1);
SELECT pg_sleep(0.03);
INSERT INTO defaults_demo (id) VALUES (2);
SELECT id,parsed,called FROM defaults_demo ORDER BY id;
SELECT min(parsed)=max(parsed) AS one_parsed_constant,
       count(DISTINCT called)=2 AS two_insert_transaction_times FROM defaults_demo;
DO $$ BEGIN
  IF (SELECT min(parsed)=max(parsed) FROM defaults_demo) IS DISTINCT FROM true THEN
    RAISE EXCEPTION 'parsed default was evaluated again';
  END IF;
END $$;
DROP TABLE defaults_demo;

On PostgreSQL 18.6, the first two rows shared tx while their statement and wall readings differed. Alias comparison returned true. Switching from UTC to Asia/Shanghai changed the visible offset to plus eight hours, while the two epoch values remained identical. Assertions check transaction stability and the unchanged instant.

After rollback, the default experiment inserted two rows in separate transactions. Both retained the same parsed literal, while called advanced between inserts. The short sleeps make the difference observable in this run, not a clock-resolution or scheduling guarantee. The final DROP removes the second temporary table; session termination also removes temporary objects if an assertion stops execution early.

Sources