Japan Standard Time (JST) is a fixed UTC+9 offset with no daylight saving time to complicate it. That simplicity makes it tempting to assume timezone handling is “just add or subtract nine hours.” In practice, most timezone bugs don’t come from getting that arithmetic wrong — they come from losing track of which basis a given timestamp is even using in the first place. This article walks through three real examples, drawn from actual code, logging, and this very blog’s own publishing workflow.
Pitfall 1: values that don’t carry their own timezone
Note: a “naive” datetime is a date/time value with no timezone information attached at all. An “aware” datetime is one that carries that information.
Python’s datetime.now() returns the current time in the server’s local timezone, but the returned datetime object itself carries no tag saying “this is JST.” datetime.utcnow() has the same problem in reverse — it returns UTC, but nothing about the value says so.
from datetime import datetime
local_now = datetime.now() # e.g. 2026-09-15 14:30:00, but nothing says "JST"
utc_now = datetime.utcnow() # e.g. 2026-09-15 05:30:00, but nothing says "UTC"
Put these two side by side and there’s no way to tell which is which without knowing how the code that produced them was written. Log one and persist the other to a database, and whoever reads it later — including you, months from now — has no way to know which basis it’s in.
That’s exactly the shape of bug that slips past review. A value that’s off by nine hours still looks like a perfectly plausible timestamp on its own; the mistake only becomes visible once you compare it against something known to be correct.
datetime.utcnow() was deprecated in Python 3.12, for precisely this reason — a naive value’s timezone isn’t self-evident. The replacement, datetime.now(timezone.utc), returns a value that carries “this is UTC” as part of itself (an aware datetime).
from datetime import datetime, timezone
utc_now = datetime.now(timezone.utc) # tzinfo=UTC travels with the value
Pitfall 2: log timestamps default to local time
An earlier article on log-level design used maintenance_agent.py‘s logging.Formatter as an example. The %(asctime)s that formatter produces is built, by default, using time.localtime — the server’s own local timezone. That’s Python’s logging module default, not something the code opts into.
_log_fmt = logging.Formatter('%(asctime)s - [%(levelname)s] - %(message)s')
Run a single server, always on JST, and this never causes a problem — the log timestamps just read as JST. But once logs from multiple servers (which may not share a timezone) get aggregated for a single timeline, or a JST server’s logs get compared against a UTC-based cloud environment’s logs during an incident, plain timestamp strings with no explicit timezone can’t be reliably lined up against each other.
logging.Formatter exposes a converter attribute that can be swapped to time.gmtime to make asctime UTC-based instead.
import time
_log_fmt = logging.Formatter('%(asctime)s - [%(levelname)s] - %(message)s')
_log_fmt.converter = time.gmtime # asctime is now UTC-based
Neither choice is inherently correct — what matters is that the basis a given log’s timestamps use is stated somewhere, in the code or the runbook, rather than left implicit.
Pitfall 3: WordPress’s post_date and post_date_gmt split
This blog’s own publishing workflow has run into exactly this category of bug. WordPress stores a post’s timestamp in two separate columns: post_date (local time, in whatever timezone the site is configured for — JST, for this blog) and post_date_gmt (the same instant, expressed in UTC).
What happens if a wp post create call only sets one of the two correctly? This blog hit that in practice: a post’s --post_date was correctly set to a past JST time, but --post_date_gmt was left unset (or ended up holding the same value as the JST one). WordPress interpreted that as “this post is scheduled for a time still in the future,” silently flipped its status to future, and the page started returning a 404 — even though the intent was to publish immediately.
# safe publish command: set both post_date (JST) and post_date_gmt (UTC) explicitly
wp post create \
--post_status=publish \
--post_date='2026-09-15 14:00:00' \
--post_date_gmt='2026-09-15 05:00:00' \
...
The root cause is simple: post_date_gmt is supposed to hold post_date minus nine hours, but if WordPress’s default logic or a missing CLI flag lets the JST value get stored there as-is, it ends up nine hours ahead of the real UTC time — which reads as “still in the future.” Nine hours happens to be exactly the JST/UTC offset, so whenever a post mysteriously gets treated as nine hours ahead of schedule, this post_date / post_date_gmt mismatch is the first thing worth checking. The fix is just as simple: always set both fields explicitly, nine hours apart, on every create or update call.
Note: WordPress keeps these as two columns because they serve different purposes —
post_dateis the human-facing time an admin sees in the dashboard, andpost_date_gmtis the timezone-independent value plugins and API integrations rely on. The REST API reflects the same split, returning bothdate(site timezone) anddate_gmt(UTC) for every post.
What these three have in common
These three pitfalls look like unrelated technologies on the surface — plain Python datetimes, a logging formatter, a pair of WordPress database columns — but they share the same root cause. Some value’s timezone basis simply isn’t recoverable from the value itself, and something downstream assumes it is.
| Pitfall | Where it shows up | Symptom |
|---|---|---|
| Naive datetimes | Python code | Comparing or storing values without knowing which basis each one is in |
| Local-time log timestamps | logging.Formatter |
Logs from different servers/environments can’t be lined up on a shared timeline |
post_date/post_date_gmt mismatch |
WordPress publishing | Post misread as nine hours in the future; status flips to future, page 404s |
How to avoid it
The fixes all point the same direction — not a clever trick, so much as consistently picking one basis and making sure every value carries it explicitly.
- Keep everything internal in UTC, and convert to local time only at the point where a human reads it — sometimes phrased as “UTC in the core, local at the edge.” Converting too early, or in more than one place, is exactly how double-conversion and missed-conversion bugs creep in
- In Python, avoid producing naive
datetimevalues in the first place; preferdatetime.now(timezone.utc)so the timezone travels with the value instead of living only in the programmer’s memory - Where a value’s basis genuinely isn’t self-evident — logs, external integrations — name it explicitly (a
_utcor_jstsuffix on a variable, a line in the docs) so the basis is visible without having to trace back through the code - For systems like WordPress that keep the same instant in two separate columns, never write a command that updates just one of them. Every create or update call should set both, together, every time
JST’s fixed UTC+9 offset with no DST makes the arithmetic trivial, but that same simplicity breeds a kind of complacency — “it’s just nine hours either way.” In practice, the failures don’t come from getting the arithmetic wrong. They come from losing track of which basis a given value was in to begin with, and that’s the thing worth watching for.