Сразу вывод: у метки времени Unix нет часового пояса. Это просто количество секунд, прошедших с 1970-01-01 00:00:00 UTC. Замеченные «8 часов разницы» почти всегда появляются на шаге, где число превращается в читаемую строку.
Что такое метка времени
Метка времени Unix — целое число, обозначающее один момент. В нём нет понятия «московское время» или «нью-йоркское»: один и тот же момент везде даёт одно и то же число. Именно поэтому хранить метки времени безопаснее всего: переезд сервера или смена часового пояса системы их не искажают.
Следить нужно только за точностью: 10 знаков — секунды, 13 — миллисекунды. Если прочитать миллисекунды как секунды, получится год 50000; если наоборот — январь 1970-го. Эта ошибка встречается в любом языке.
Три источника разницы в 8 часов
1. Хранение и отображение используют разные соглашения. Бэкенд хранит UTC, фронтенд форматирует в локальное время — оба правы, просто никто не уточнил, какой пояс имелся в виду.
2. Сервер работает не в UTC. Одни образы по умолчанию используют Asia/Shanghai, другие UTC. Один и тот же код на двух машинах пишет строки лога с разницей в 8 часов. Если время в логах не сходится, сначала проверьте date и timedatectl.
3. При разборе строки подставляется часовой пояс. Значение вида 2026-10-19 00:00:00 без смещения браузеры и Node читают как локальное время, а часть библиотек — как UTC. Одна строка, два прочтения, 8 часов разницы.
Быстрая самопроверка
Если не уверены, сделайте две вещи: прочитайте одну и ту же метку как UTC и как локальное время — разница в целое число часов означает проблему пояса; затем переведите ожидаемое время в метку и сравните со значением в логе. Разница 28 800 секунд — это ровно 8 часов.
Три привычки, снимающие большинство проблем
- Хранить метки в UTC или ISO 8601 со смещением (например,
2026-10-19T02:00:00Z) - При отображении указывать пояс явно и не полагаться на настройки окружения
- В API всегда передавать смещение или прямо указывать, что значение в UTC
Одной фразой
Хранить в UTC, показывать в поясе пользователя, разбирать с явным смещением. Эти три правила снимают большинство жалоб «время неверное».
Один момент — пять способов записи
Запишите один и тот же момент — 2026-10-19 02:00:00 UTC — пятью разными способами, и источник путаницы станет очевиден:
1792375200— секунды Unix. Абсолютно, однозначно.1792375200000— миллисекунды Unix. Абсолютно, однозначно.2026-10-19T02:00:00Z— ISO 8601, завершающаяZозначает UTC. Абсолютно.2026-10-19T10:00:00+08:00— ISO 8601 с явным смещением. Абсолютно.2026-10-19 10:00:00— без указания зоны вообще. Это не момент времени: значение означает ту локальную зону, в которой работает разбирающая его программа.
Первые четыре указывают на одну и ту же точку на оси времени. Пятая на каждой машине указывает на свою. Почти все споры «время не сходится» возникают из-за того, что пятую форму используют как одну из первых четырёх.
Значения по умолчанию различаются
Именно здесь возникают ошибки. Одно и то же намерение — «сохранить время» — в каждом слое реализовано по-своему:
- MySQL:
TIMESTAMPпри записи пересчитывает значение в UTC по часовому поясу сессии, а при чтении пересчитывает обратно.DATETIMEхранит ровно то, что передали. Неверный тип столбца — и клиент в другом поясе прочитает другое значение. - PostgreSQL:
timestamptzхранит UTC, сохраняя семантику зоны;timestampзону не хранит. Документация рекомендует по умолчанию первый вариант. - Java:
Instant— абсолютная точка,LocalDateTimeзоны не несёт. СериализуйтеLocalDateTimeпрямо в строку — и информация о зоне исчезнет. - Python:
datetimeбывает naive (без зоны) и aware (с зоной), и вычитание одного из другого выбрасывает исключение. Присоединение зоны черезzoneinfo— современный стандарт. - JavaScript: внутри
Dateвсегда лежит миллисекундная метка, ноgetHours()иtoString()выводят в зоне окружения, аtoISOString()— всегда в UTC.
Одно правило покрывает всё: любой тип, который называется «локальное время», не содержит информации о зоне и искажается, как только пересекает границу машины.
Не сокращения, а имена IANA
Аббревиатура CST может означать китайское стандартное время (UTC+8), центральное стандартное время США (UTC−6) или кубинское стандартное время (UTC−5). EST не безопаснее: он не учитывает летнее время, тогда как восточное побережье США летом живёт по EDT (UTC−4).
Правильно писать идентификаторы IANA: Asia/Shanghai, America/New_York, Europe/Berlin. В них зашита полная история переходов на летнее время, их поддерживает системная база tz, и они остаются верными для любых годов.
Легко забываемая деталь: в Китае летнее время действовало с 1986 по 1991 год, поэтому летние моменты тех лет находятся в UTC+9. Asia/Shanghai учтёт это правильно, а жёстко вписанное UTC+8 ошибётся на час.
Три вопроса, прежде чем читать код
- Это момент времени или показание настенных часов? Если число — ошибка неизбежно в форматировании. Если строка — сначала выясните, в какой зоне её записали.
- Сходятся ли в зоне производитель, хранилище и браузер? Если нет, где-то есть пересчёт — он и есть подозреваемый.
- Разница кратна целому числу часов? Восемь часов или 5,5 часа (Индия, UTC+5:30) — это зона. Расхождение в секунды скорее синхронизация часов или точность.
Эти три вопроса отсекают половину вариантов ещё до того, как вы откроете первый лог.