Aller au contenu
Mohamed Tahiri
Miami --:--
← Tous les articles

Stocker une date future : pourquoi l’UTC ne suffit pas

« Tout stocker en UTC » est un bon réflexe pour le passé, et un piège pour les rendez-vous à venir. Trois changements de fuseau bien réels, et ce qu’ils font à une base de données.

Je vis à Miami et je travaille avec Paris. Six heures d’écart, sauf quelques semaines par an. Les États-Unis changent d’heure le deuxième dimanche de mars et le premier dimanche de novembre, l’Europe le dernier dimanche de mars et le dernier dimanche d’octobre. En 2026, l’écart est tombé à cinq heures du 8 au 28 mars, et il y retombera du 25 au 31 octobre. Ces semaines-là, une invitation calculée de tête se trompe facilement d’une heure.

Les changements d’heure ne sont pourtant que la partie prévisible du problème. Les règles elles-mêmes changent, décidées par des gouvernements, parfois avec quelques jours de préavis. Et c’est là que le conseil le plus répandu sur les dates, « stockez tout en UTC », cesse d’être vrai.

Pour le passé, l’UTC a raison

Un paiement, une connexion, un message envoyé : ces événements ont eu lieu à un instant précis, et l’UTC décrit un instant sans ambiguïté. On stocke l’instant, on convertit à l’affichage dans le fuseau de la personne qui regarde, et aucune décision politique ne peut changer le passé.

Pour le futur, l’heure locale fait foi

Un rendez-vous chez le dentiste mardi prochain à 10:00 n’est pas un instant. C’est une heure sur l’horloge d’un lieu. L’instant correspondant dépend des règles de fuseau en vigueur ce jour-là, et ces règles peuvent changer entre la réservation et le rendez-vous.

Convertir en UTC au moment de la réservation revient à figer les règles d’aujourd’hui dans la base. Le jour où elles changent, l’instant stocké est toujours là, parfaitement précis, et faux. Choisissez une ville, puis appliquez la mise à jour des règles :

Trois cas réels

Le Mexique a aboli l’heure d’été à l’automne 2022, quelques jours seulement après le vote de la loi. Un rendez-vous pris pendant l’été pour juillet 2023, à 10:00 heure de Mexico, avait été converti avec l’ancienne règle, UTC−5. Après la réforme, Mexico reste à UTC−6 toute l’année : l’instant stocké tombe à 9:00.

L’Égypte a fait l’inverse au printemps 2023 en rétablissant l’heure d’été, annoncée quelques semaines avant. Les rendez-vous de l’été suivant, convertis avec UTC+2, s’affichent alors une heure trop tard. Et le 1ᵉʳ mars 2024, le Kazakhstan a aligné tout le pays sur UTC+5 : Almaty, qui était à UTC+6, a reculé d’une heure.

Ces changements arrivent jusqu’aux machines par la base de données des fuseaux de l’IANA, publiée plusieurs fois par an, puis par les mises à jour du système, des langages et des navigateurs. Il y a toujours un délai entre la décision et le serveur. Une raison de plus de ne pas convertir trop tôt.

Ce que je stocke

Pour un événement futur à heure locale, trois colonnes. L’heure locale, sans fuseau. L’identifiant IANA du lieu, comme America/Mexico_City, jamais une abréviation comme CST ni un décalage comme -06:00 : un décalage est justement ce qui change. Et l’instant calculé, utile pour trier et pour les requêtes, mais traité comme un cache.

meetings.sql
create table meetings (
  id           bigint generated always as identity primary key,
  starts_local timestamp   not null, -- wall-clock time, no zone
  time_zone    text        not null, -- IANA id, e.g. 'America/Mexico_City'
  starts_at    timestamptz not null  -- derived instant, recomputed when tz rules change
);

L’instant se recalcule à la lecture, ou par un job lancé après chaque mise à jour des données de fuseaux. Ce job peut aussi comparer l’ancien instant au nouveau et prévenir les participants concernés.

meetings.py
from datetime import datetime
from zoneinfo import ZoneInfo

def resolve(starts_local: datetime, time_zone: str) -> datetime:
    """Turn a wall-clock time into an instant, with the rules installed today."""
    return starts_local.replace(tzinfo=ZoneInfo(time_zone))

meeting = resolve(datetime(2027, 7, 12, 10, 0), "America/Mexico_City")
meeting.isoformat()                                   # '2027-07-12T10:00:00-06:00'
meeting.astimezone(ZoneInfo("Europe/Paris")).isoformat()  # '2027-07-12T18:00:00+02:00'

Les cas qui restent à trancher

Stocker l’heure locale règle le problème principal, et en fait apparaître d’autres, qu’il vaut mieux décider explicitement que découvrir en production.

L’heure qui n’existe pas

Le jour où les horloges avancent, 2:30 n’existe pas dans beaucoup de fuseaux. Il faut une règle : décaler après le saut, ou refuser à la saisie.

L’heure qui existe deux fois

Le jour où elles reculent, 1:30 se produit deux fois à New York. Python l’exprime avec l’attribut fold ; dans tous les cas, choisir et documenter.

Les événements récurrents

Une règle de récurrence, une heure locale et un fuseau. On calcule les occurrences à la lecture, jamais à l’avance pour des années.

Le fuseau de référence

Une visio entre Paris et Miami appartient au fuseau de celui qui l’organise. Ce choix doit être stocké, pas deviné.

J’ai rencontré plusieurs de ces cas en construisant les SDK d’une API d’horaires, où des milliers de lieux publient leurs heures en heure locale, jour par jour, chacun dans son fuseau. Côté JavaScript, l’API Temporal, qui arrive dans les navigateurs, sépare enfin les deux notions : PlainDateTime pour l’heure murale, ZonedDateTime pour l’heure murale rattachée à un lieu.

La règle courte

  • Passé : un instant UTC.
  • Futur à heure locale : heure locale + identifiant IANA, et l’instant en cache.
  • Jamais un décalage fixe pour représenter un lieu.
  • Les données de fuseaux sont une dépendance : on les met à jour, et on recalcule après.

Le temps reste le domaine le plus traître que je connaisse. Mais la plupart des bugs viennent d’une seule confusion, entre un instant et une heure affichée sur une horloge.

Contact

Construisons quelque chose.

Ou laissez un message