Timezones
A timestamp on its own — a plain number of seconds since 1970 — has no timezone, but the moment you format it as a human-readable date and time, a timezone has to be chosen to interpret it against. Get that choice wrong, or let it vary unexpectedly between a server and the people using an application, and dates start appearing hours off from where they should be. This is one of the most common and most avoidable classes of bugs in web applications.
PHP's default timezone
If no timezone is set anywhere, PHP falls back to a default defined by the date.timezone setting in php.ini (or UTC if that's also unset, as of PHP 5.4+, though older configurations may still warn about it). Relying on the php.ini default is risky because it silently varies between local development machines, staging servers, and production hosts. date_default_timezone_set() lets a script pin the timezone explicitly at runtime, independent of whatever the server happens to be configured with.
Setting the default timezone explicitly
<?php
date_default_timezone_set('UTC');
echo date('Y-m-d H:i:s'), "\n";
echo date_default_timezone_get(), "\n";2025-06-15 09:00:00 UTC
This call should happen once, early, near the top of an application's bootstrap — not scattered across individual scripts — so every part of the codebase agrees on the same baseline.
DateTimeZone: attaching a specific zone to a date
Rather than relying on the process-wide default, the object-oriented date API lets a specific DateTimeZone be attached to an individual DateTime/DateTimeImmutable instance. This is the better approach whenever an application deals with more than one timezone at once — for example, showing each user a timestamp in their own local time.
Creating a date in a specific timezone
<?php
$utc = new DateTimeZone('UTC');
$tokyo = new DateTimeZone('Asia/Tokyo');
$meeting = new DateTimeImmutable('2025-06-15 09:00:00', $utc);
echo $meeting->format('Y-m-d H:i:s T'), "\n";
$inTokyo = $meeting->setTimezone($tokyo);
echo $inTokyo->format('Y-m-d H:i:s T'), "\n";2025-06-15 09:00:00 UTC 2025-06-15 18:00:00 JST
Both variables represent the exact same instant in time — -> setTimezone() doesn't shift "when" the moment is, it only changes
which local clock reading is used to display it. Timezone names
like Asia/Tokyo and America/New_York come from the IANA
timezone database, which is preferable to fixed offsets like
+09:00 because it automatically accounts for daylight saving
rules that fixed offsets can't express.
Store in UTC, convert only for display
The pattern that avoids the most bugs is simple to state: persist every date and time in a database as UTC, do all internal comparisons and calculations in UTC, and convert to a specific user's local timezone only at the very last moment, when rendering something for a human to read.
Store in UTC, display in the user's zone
<?php
// Value coming out of the database, already UTC
$storedUtc = new DateTimeImmutable('2025-03-10 02:30:00', new DateTimeZone('UTC'));
function displayForUser(DateTimeImmutable $utcDate, string $userTimezone): string
{
$local = $utcDate->setTimezone(new DateTimeZone($userTimezone));
return $local->format('F j, Y \a\t g:i A T');
}
echo displayForUser($storedUtc, 'America/New_York'), "\n";
echo displayForUser($storedUtc, 'Europe/London'), "\n";March 9, 2025 at 9:30 PM EST March 10, 2025 at 2:30 AM UTC
Notice the New York reading even lands on a different calendar day than the UTC value — this is exactly the kind of detail that gets lost when timezone handling is bolted on as an afterthought instead of designed in from the start.
Daylight saving time and ambiguous local times
Twice a year, in timezones that observe daylight saving time, a local clock either skips an hour or repeats one. That repeated hour creates a genuinely ambiguous local time — "2:30 AM" during a fall-back transition happened twice, and without extra context there's no way to know which occurrence was meant. This is precisely why storing everything in UTC matters: UTC has no daylight saving rules, so a UTC timestamp is always unambiguous, and the ambiguity only ever appears at the display layer, where it's usually harmless.
date_default_timezone_set()sets the process-wide default; call it once, early, rather than relying onphp.ini.DateTimeZoneattaches an explicit timezone to an individual date object, independent of the process default.->setTimezone()reinterprets the same instant for display in a different zone — it does not change what instant is represented.IANA zone names (
America/New_York) correctly track daylight saving rules; fixed offsets (+09:00) do not.Store and compute in UTC; convert to a local zone only when displaying to a specific user.