WordPress Date and Time: current_time, wp_date, Timezones, and UTC Handling

Date and time handling is one of the most error-prone areas of WordPress development, because WordPress stores dates in two places simultaneously: the raw UTC value in post_date_gmt and a site-timezone-adjusted value in post_date. The site’s configured timezone (Settings → General → Timezone) determines the offset, but the database always stores UTC in the _gmt columns. PHP’s own timezone setting (date_default_timezone_set()) is set to UTC by WordPress at startup — which means calling PHP’s native date() or new DateTime() without an explicit timezone object gives UTC, not the site’s local time. WordPress provides its own time functions to bridge this gap: current_time(), wp_date(), and get_the_date() all apply the site’s timezone correctly. Understanding when to use which function — and when to use UTC deliberately — prevents a whole category of timezone-related bugs.

Problem: Your plugin schedules a WP-Cron job to run "at midnight site time", stores timestamps in post meta, and displays formatted dates in admin pages — but the dates are off by several hours because the code mixes UTC and local time.

Solution: Use wp_date() for display formatting (applies site timezone), current_time( 'timestamp' ) or current_time( 'mysql' ) for site-local values, and Unix timestamps (UTC) for arithmetic and WP-Cron scheduling. Store timestamps as UTC integers in post meta.

<?php
// ── Get the current time in various formats ────────────────────────────

// Site-local MySQL datetime (e.g. for post_date field)
$local_mysql = current_time( 'mysql' );          // '2020-01-20 14:30:00'

// UTC MySQL datetime (e.g. for post_date_gmt field)
$utc_mysql   = current_time( 'mysql', true );    // '2020-01-20 12:30:00' (if UTC+2)

// Unix timestamp (always UTC) — use for arithmetic and scheduling
$utc_ts      = time();                           // 1579520400
$local_ts    = current_time( 'timestamp' );      // UTC offset applied — avoid for math

// ── Format a timestamp for display (site timezone) ────────────────────
// wp_date() = date_i18n() replacement, uses site timezone, translates month names
echo wp_date( 'F j, Y', $utc_ts );  // 'January 20, 2020' in site local time

// For a post's published date, always use these template functions:
echo get_the_date( 'j F Y' );          // uses post_date (site-local)
echo get_the_modified_date( 'j F Y' ); // uses post_modified (site-local)

// ── Converting between UTC and local time ─────────────────────────────
$site_tz = wp_timezone();  // returns a DateTimeZone object for the site's timezone

// Convert a UTC timestamp to a DateTimeImmutable in site timezone
$dt_local = new DateTimeImmutable( '@' . $utc_ts );
$dt_local = $dt_local->setTimezone( $site_tz );
echo $dt_local->format( 'Y-m-d H:i:s' ); // local time

// Convert a local datetime string to UTC timestamp
$dt_local_str = '2020-01-20 14:30:00';
$dt = new DateTimeImmutable( $dt_local_str, $site_tz );
$utc_timestamp = $dt->getTimestamp(); // UTC Unix timestamp

// ── WP-Cron scheduling: always use UTC timestamps ─────────────────────
// Schedule for "midnight tonight" in site local time:
$site_tz       = wp_timezone();
$midnight_local = new DateTimeImmutable( 'tomorrow midnight', $site_tz );
$midnight_utc   = $midnight_local->getTimestamp(); // UTC

if ( ! wp_next_scheduled( 'my_plugin_midnight_job' ) ) {
    wp_schedule_event( $midnight_utc, 'daily', 'my_plugin_midnight_job' );
}

// ── Store and retrieve timestamps in post meta ────────────────────────
// Store as UTC Unix timestamp (integer)
update_post_meta( $post_id, 'event_start', $midnight_utc );

// Retrieve and display
$ts = (int) get_post_meta( $post_id, 'event_start', true );
echo wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $ts );

NOTE: current_time( 'timestamp' ) returns a UTC Unix timestamp with the site's UTC offset added to it — meaning it is not a true Unix timestamp. This is a long-standing quirk of WordPress and it exists for historical reasons. For any arithmetic (calculating durations, comparing timestamps, scheduling) use time() which returns a real UTC Unix timestamp. Use current_time( 'timestamp' ) only when you need a "local Unix timestamp" for comparison against other values that were also generated with the same function. The wp_date() function (added in WP 5.3) is now preferred over date_i18n() for display formatting, as it correctly handles the site timezone via a DateTimeZone object rather than the offset.