Почему время в логах всегда расходится на 8 часов?

У метки времени нет собственного часового пояса. Расхождение в 8 часов почти всегда возникает при преобразовании числа в читаемое время.

Сразу вывод: у метки времени 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) — это зона. Расхождение в секунды скорее синхронизация часов или точность.

Эти три вопроса отсекают половину вариантов ещё до того, как вы откроете первый лог.

Связанные инструменты