Справочник · Данные и форматы
Временные зоны: как хранить и показывать время без ошибок
UTC, смещение и зона — разные вещи. Разбираем ISO 8601, базу часовых поясов, переход на летнее время и работу со временем в Linux, Python, JavaScript и PostgreSQL.
Время кажется простым, пока приложение живёт в одном городе. Как только появляются пользователи из Калининграда и Владивостока, сервер в другой стране и отчёты «за вчера», начинаются ошибки на час, на день и на целый часовой пояс. Почти все они лечатся несколькими правилами.
Три понятия, которые путают
| Понятие | Пример | Что это |
|---|---|---|
| момент времени | 2026-11-03T09:00:00Z | точка на общей шкале, одинаковая для всех |
| смещение | +03:00 | на сколько местное время отличается от UTC прямо сейчас |
| часовой пояс | Europe/Moscow | правила региона: смещения и их история, включая переходы на летнее время |
Смещение и зона — не одно и то же. +01:00 зимой — это и Берлин, и Париж, и Лагос, но летом Берлин живёт по +02:00, а Лагос остаётся на +01:00. Поэтому для будущих событий нужно хранить зону, а не смещение.
Unix-время — число секунд с 1 января 1970 года 00:00 UTC. Оно не зависит от поясов вовсе: в момент, когда вы читаете эту строку, оно одинаково в Москве и Нью-Йорке. Перевести его в дату и обратно можно в конвертере unix-времени.
Формат ISO 8601
2026-11-03 дата
2026-11-03T14:05:00Z момент в UTC, Z означает нулевое смещение
2026-11-03T17:05:00+03:00 тот же момент с московским смещением
2026-11-03T14:05:00.123Z с миллисекундами
2026-W45 неделя года
P1DT2H30M длительность: 1 день, 2 часа, 30 минутДаты в таком виде сортируются как строки, не зависят от локали и однозначно читаются любым языком программирования. Форматы вроде 03.11.2026 или 11/03/2026 оставьте для отображения людям: второй в США значит 3 ноября, а в Европе — 11 марта.
База часовых поясов
Правила поясов меняются по решению правительств, иногда с уведомлением за несколько недель. Их собирает база IANA tz (её ещё называют tzdata или Olson). Зоны в ней называются по региону и крупному городу: Europe/Moscow, Asia/Yekaterinburg, America/New_York.
| Зона | Смещение сейчас | Примечание |
|---|---|---|
Europe/Kaliningrad | +02:00 | |
Europe/Moscow | +03:00 | без перехода на летнее время с 26 октября 2014 года |
Europe/Samara | +04:00 | |
Asia/Yekaterinburg | +05:00 | |
Asia/Novosibirsk | +07:00 | |
Asia/Vladivostok | +10:00 | |
Asia/Kamchatka | +12:00 |
Обновления tzdata приходят с пакетами системы (tzdata в Linux), а у Java, Python-пакетов вроде tzdata и браузеров — своими путями. Если правила в стране поменялись, обновить нужно везде, где время переводится в местное.
Летнее время и его ловушки
Во многих странах дважды в год часы переводят. Например, в Берлине в последнее воскресенье марта в 02:00 по местному времени часы сразу переводят на 03:00, а в последнее воскресенье октября в 03:00 их возвращают на 02:00, и этот час проходит дважды.
Из этого следует:
- Несуществующее время.
2026-03-29 02:30в Берлине не было. Библиотеки по-разному обрабатывают такой ввод: сдвигают вперёд или выдают ошибку. - Двойное время.
2026-10-25 02:30в Берлине было дважды. Без смещения неясно, какой из моментов имеется в виду. - Сутки не всегда 24 часа. Прибавить «1 день» и «24 часа» — разные операции в дни перехода.
- Расписания. Задача cron на 02:30 по местному времени в день перехода может не выполниться или выполниться дважды. Поэтому серверные расписания лучше задавать в UTC, о cron подробнее — в отдельной статье.
Правила хранения
- Моменты, которые уже произошли (создание заказа, вход в систему, лог) — храните в UTC. В базе — тип с часовым поясом или unix-время.
- Будущие события, привязанные к месту («встреча в 10:00 по Москве через полгода»), — храните местное время и имя зоны. Если правила пояса поменяются, момент пересчитается правильно.
- Даты без времени (день рождения, дата документа) — храните как дату, без зоны и без полуночи. Иначе день рождения «уедет» на день назад у пользователя западнее сервера.
- Переводите время в местное только при показе пользователю, по его зоне.
- Логи и серверы — в UTC. Так проще сопоставлять события из разных мест.
Linux и командная строка
timedatectl # зона системы, синхронизация часов
timedatectl list-timezones | grep Asia
sudo timedatectl set-timezone UTC
date -u +%Y-%m-%dT%H:%M:%SZ # текущее время в UTC в ISO 8601
date +%s # unix-время
date -d @1767225600 # unix-время в дату (GNU date)
TZ=Asia/Vladivostok date # который час во Владивостоке
date -u -d 'TZ="Europe/Moscow" 2026-11-03 10:00' # московское время в UTCНа macOS команда date другая: вместо -d @1767225600 пишут -r 1767225600.
Python
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
now = datetime.now(timezone.utc) # всегда с зоной
local = now.astimezone(ZoneInfo("Asia/Yekaterinburg"))
meeting = datetime(2026, 12, 1, 10, 0, tzinfo=ZoneInfo("Europe/Moscow"))
meeting.astimezone(timezone.utc).isoformat() # '2026-12-01T07:00:00+00:00'
datetime.fromisoformat("2026-11-03T14:05:00Z") # Python 3.11+ понимает ZJavaScript
Объект Date хранит момент в миллисекундах UTC, а показывает его в зоне браузера. Ловушки:
| Код | Что получится |
|---|---|
new Date("2026-11-03") | полночь UTC: в Москве это 03:00, а в Нью-Йорке ещё 2 ноября |
new Date("2026-11-03T00:00") | полночь по местному времени |
new Date(2026, 10, 3) | 3 ноября: месяцы считаются с нуля |
Date.now() | миллисекунды, а не секунды, как в unix-времени |
Для показа есть встроенный Intl, а последняя строка примера возвращает зону пользователя:
new Intl.DateTimeFormat("ru-RU", {
dateStyle: "long", timeStyle: "short", timeZone: "Asia/Vladivostok"
}).format(new Date());
Intl.DateTimeFormat().resolvedOptions().timeZone;Новый стандартный API Temporal разделяет моменты, даты, время и зоны на отдельные типы и решает большинство этих проблем. Он появляется в браузерах постепенно, до тех пор выручают библиотеки date-fns-tz и Luxon.
PostgreSQL
SHOW timezone;
SET timezone = 'UTC';
-- timestamptz хранит момент в UTC и переводит при выводе
SELECT now(), now() AT TIME ZONE 'Asia/Novosibirsk';
-- начало суток по Москве, переведённое в момент времени
SELECT date_trunc('day', now() AT TIME ZONE 'Europe/Moscow') AT TIME ZONE 'Europe/Moscow';Тип timestamptz (с зоной) почти всегда правильнее, чем timestamp без зоны. Название обманчивое: имя зоны он не хранит, он хранит момент в UTC.