Tools mentioned in this article
Open the browser-based tool while you read and try the workflow immediately.
Adding nine hours is easy — knowing which side you’re on is not
The relationship between UTC and JST is refreshingly simple: JST = UTC + 9, always. Japan does not observe daylight saving time, so the offset never shifts with the season. Compared with European or North American zones, the arithmetic is something you can do in your head.
Timezone incidents still happen constantly, and the reason is not the arithmetic. It’s that you lose track of which side a given timestamp is on. When logs are UTC, the database is JST, CI is UTC, and your laptop is JST, the string 2026-08-06 09:00:00 tells you nothing on its own.
This is a reference for the conversion itself, plus the specific places where it goes wrong.
Offset cheat sheet
| UTC | JST (same day) | Note |
|---|---|---|
| 00:00 | 09:00 | Midnight UTC is 9 AM in Japan |
| 03:00 | 12:00 | Japanese noon |
| 09:00 | 18:00 | End of the Japanese work day |
| 12:00 | 21:00 | |
| 15:00 | next day 00:00 | the date rolls over here |
| 18:00 | next day 03:00 | |
| 21:00 | next day 06:00 |
Going the other way (JST → UTC), subtract nine hours. JST 00:00–08:59 falls on the previous day in UTC.
The date boundary sits at UTC 15:00. Almost every off-by-one-day bug in daily batches and reports comes from crossing that line. If you want “one day of Japanese time,” in UTC that range is previous day 15:00 → current day 15:00.
What Z and +09:00 mean in ISO 8601
| Notation | Meaning |
|---|---|
2026-08-06T00:00:00Z | Midnight UTC. Z stands for UTC (Zulu time) |
2026-08-06T09:00:00+09:00 | The exact same instant, written in JST |
2026-08-06T09:00:00 | No offset — ambiguous. Interpretation is up to the parser |
2026-08-06 | Date only. Time and zone semantics vary by implementation |
The first two describe the same instant. As long as an offset is present, nothing is lost by writing it either way.
The third row is the dangerous one. A datetime string without an offset has no meaning on its own. When you see that shape in an API response or a CSV export, go read the spec before assuming.
When JavaScript shifts your dates
This is the trap you are most likely to hit. Two strings that look like the same date parse to different instants.
// A date-only string is parsed as UTC
new Date('2026-08-06').toISOString();
// => '2026-08-06T00:00:00.000Z' (9 AM on Aug 6 in JST)
// A datetime string with no offset is parsed as LOCAL time
new Date('2026-08-06T00:00:00').toISOString();
// => '2026-08-05T15:00:00.000Z' (run in JST — note the date moved back)
Appending T00:00:00 shifts the result by nine hours and changes the date. This is specified behavior, not a bug: date-only is UTC, datetime-without-offset is local.
The fix is to always be explicit:
new Date('2026-08-06T00:00:00+09:00').toISOString();
// => '2026-08-05T15:00:00.000Z' (intended: midnight JST on Aug 6)
To render in JST, pass the zone to toLocaleString so the result no longer depends on where the code runs:
const d = new Date('2026-08-06T00:00:00Z');
d.toLocaleString('en-US', { timeZone: 'Asia/Tokyo' });
// => '8/6/2026, 9:00:00 AM' — identical on a UTC server and a JST laptop
Remember that toISOString() always emits UTC. Using it on what you think of as a local time silently stores a value nine hours off.
Server-side conversion
Python
from datetime import datetime, timezone
from zoneinfo import ZoneInfo # Python 3.9+
now_utc = datetime.now(timezone.utc)
now_jst = now_utc.astimezone(ZoneInfo('Asia/Tokyo'))
# Rule of thumb: never let naive datetimes (no tzinfo) into your system
MySQL
SELECT CONVERT_TZ('2026-08-06 00:00:00', '+00:00', '+09:00');
-- => 2026-08-06 09:00:00
The thing to watch in MySQL is that TIMESTAMP columns are converted using the session time zone while DATETIME columns are never converted at all. Mixing both types in one table means two columns that look alike but mean different things. Named zones ('Asia/Tokyo') require the timezone tables to be loaded, so explicit offsets are the safer bet.
PostgreSQL
SELECT TIMESTAMPTZ '2026-08-06 00:00:00+00' AT TIME ZONE 'Asia/Tokyo';
-- => 2026-08-06 09:00:00
The idiomatic PostgreSQL approach is to store TIMESTAMPTZ and convert with AT TIME ZONE at display time. Type choices across databases are collected in the CREATE TABLE reference.
Four places this breaks in production
1. Cron runs in the server’s timezone
GitHub Actions schedule is always UTC. Writing 0 9 * * * because you want 9 AM in Japan gets you 6 PM instead.
on:
schedule:
# 9 AM JST = midnight UTC
- cron: '0 0 * * *'
For the cron syntax itself see how to write cron expressions, and for workflow dependencies see GitHub Actions needs patterns.
2. Docker containers default to UTC
Most base images ship without timezone configuration and run in UTC. That is why log timestamps differ by nine hours between your JST laptop and the container. If your app treats times as “local,” its behavior changes with the environment.
3. Logs in mixed timezones
Access logs in server local time, application logs in UTC, and a monitoring dashboard rendering in the browser’s zone is a completely normal state of affairs. When correlating logs during an incident, normalize everything to UTC or Unix time first. A Unix timestamp has no timezone at all — it is an absolute count of seconds, which makes it the reliable common denominator (see what Unix time is).
4. Storing “just a date”
A value like 2026-08-06 covers a different span depending on the zone. Business dates — billing cutoffs, reporting days — are safer kept as dates in a DATE column. The moment you widen one into a datetime, you have to answer “midnight in which timezone?”
The rule: store UTC, convert on display
Condensed into three lines:
- Store and compute in UTC (or Unix time)
- Convert to JST only when rendering
- Always include an offset in strings (
Zor+09:00)
“We only serve Japan, so we store JST” sounds reasonable until you notice that CI and your containers run in UTC. Every boundary is a place to make a mistake; fewer boundaries, fewer mistakes.
Converting on the spot
When you need to know what a UTC timestamp from a log means in JST, paste it into the UTC/JST converter and get both directions at once. It handles ISO 8601 input and can grab the current time for you.
For Unix timestamps (numbers like 1785974400) use the Unix time converter, and to read timeout or TTL settings across seconds, minutes, and hours, use time unit conversion. All of them run entirely in your browser — the timestamps you enter are never sent anywhere.
Summary
- JST = UTC + 9, fixed. Japan has no daylight saving time
- The date boundary is UTC 15:00. “One Japanese day” is previous-day 15:00 → current-day 15:00 in UTC
- An ISO 8601 string with no offset (
Z/+09:00) is ambiguous by itself - JavaScript parses date-only as UTC and datetime-without-offset as local
- MySQL converts
TIMESTAMPbut neverDATETIME - GitHub Actions cron is always UTC; 9 AM JST is
0 0 * * * - Store UTC, convert on display, always write the offset
FAQ
Does the UTC–JST offset change with the seasons?
No. Japan Standard Time is fixed at UTC+9 and Japan does not observe daylight saving time, so the difference is nine hours year-round. For European or North American zones, where DST does apply, avoid hard-coding an offset and use a zone name such as Asia/Tokyo instead.
Are 2026-08-06T09:00:00+09:00 and 2026-08-06T00:00:00Z different times?
They are the same instant, written in two different zones. Neither notation loses information, but picking one canonical form inside your system saves confusion when comparing and sorting.
Why does my date shift by a day in JavaScript?
Because a date-only string ('2026-08-06') is parsed as UTC while a datetime string without an offset ('2026-08-06T00:00:00') is parsed as local time. In a JST environment the latter resolves to nine hours earlier (15:00 UTC on the previous day), so printing it in UTC shows the previous date. Always include an offset in the string.
How do I run a GitHub Actions job at 9 AM Japan time?
Use - cron: '0 0 * * *', because schedule is interpreted in UTC and 9 AM JST is midnight UTC. Note that scheduled runs on GitHub Actions can be delayed by several minutes under load, so avoid them when minute-level precision matters.
Are the timestamps I paste in sent to a server?
No. Both the UTC/JST converter and the Unix time converter run entirely in your browser — nothing you enter is transmitted anywhere.