Timestamps Unix en production : secondes vs millisecondes, le problème de 2038 et les bugs de fuseaux horaires
Tout système en production finit par journaliser, stocker ou transmettre un timestamp Unix. Et tout système en production finit par livrer un bug qui en implique un — une date qui s’affiche en janvier 1970, un événement planifié 49 ans dans le passé, une intégration d’API où un côté attend des millisecondes et l’autre envoie des secondes. Ces bugs sont ennuyeux, fréquents et entièrement évitables une fois que l’on connaît la poignée de règles qui régissent le fonctionnement réel des timestamps.
Ce guide couvre le cœur pratique : reconnaître l’unité d’un timestamp à son nombre de chiffres, le problème de 2038 et son état réel aujourd’hui, les schémas de bugs qui se produisent vraiment, et la bonne façon d’obtenir un timestamp dans les langages que vous utilisez probablement.
Ce qu’un timestamp Unix est (et n’est pas)
Un timestamp Unix compte le nombre de secondes écoulées depuis le 1er janvier 1970 à 00:00:00 UTC — l’« epoch Unix ». Deux propriétés comptent pour tout ce qui suit :
- Il n’a pas de fuseau horaire. Le timestamp
1785235837désigne un instant précis, partout sur Terre, simultanément. Les fuseaux horaires n’entrent en jeu que lorsque vous affichez un timestamp à un humain ou que vous en construisez un à partir de composantes d’heure locale. Si vous ne retenez qu’une phrase de cet article, que ce soit celle-ci : les timestamps sont des instants UTC ; les fuseaux horaires sont une question d’affichage. - Il ignore les secondes intercalaires. Le temps Unix suppose que chaque jour compte exactement 86 400 secondes. Quand une seconde intercalaire est insérée, le temps POSIX la répète ou l’étale de fait (le comportement varie selon les systèmes ; le « leap smear » de Google l’étale sur la journée). Pour la plupart du code applicatif, c’est sans importance ; pour des calculs d’intervalles qui enjambent une seconde intercalaire, cela signifie que votre hypothèse « exactement N secondes » peut être fausse d’une unité.
Notez aussi que le choix de l’epoch rend les timestamps négatifs valides : -1 correspond au 31 décembre 1969 à 23:59:59 UTC. Un code qui traite les timestamps négatifs comme des erreurs casse sur les dates antérieures à 1970 — un vrai schéma de bug pour les systèmes manipulant des données historiques (dates de naissance, archives).
Secondes, millisecondes, microsecondes : l’heuristique du nombre de chiffres
Les timestamps arrivent dans différentes unités selon les écosystèmes, et mal identifier l’unité est le bug de timestamp le plus courant. Le test rapide de terrain consiste à compter les chiffres :
| Unité | Exemple (mi-2026) | Chiffres | Quand cette largeur est atteinte |
|---|---|---|---|
| Secondes | 1785235837 | 10 | 10 chiffres depuis le 09/09/2001 ; 11 chiffres à partir de 2286 |
| Millisecondes | 1785235837000 | 13 | 13 chiffres depuis le 09/09/2001 |
| Microsecondes | 1785235837000000 | 16 | 16 chiffres |
| Nanosecondes | 1785235837000000000 | 19 | dépasse la plage int64 en 2262 |
Règles empiriques pour l’ère actuelle (2001–2286) :
- 10 chiffres → secondes. Toujours sûr à supposer.
- 13 chiffres → millisecondes.
Date.now()en JavaScript,System.currentTimeMillis()en Java, et la plupart des systèmes de logs et de métriques émettent ces valeurs. - 16 chiffres → microsecondes.
time.time()en Python multiplié, certaines bases de données, certains systèmes de traçage. - 19 chiffres → nanosecondes.
time.Now().UnixNano()en Go, et celui-ci est un piège : 19 chiffres dépassent le maximum d’un entier 64 bits signé (9 223 372 036 854 775 807) après 2262, mais aujourd’hui un nombre à 19 chiffres tient dans un int64 — tout juste. Si vous stockez des timestamps en nanosecondes, ne faites pas d’arithmétique qui suppose de la marge.
Un contrôle rapide de cohérence en débogage : collez le nombre dans un convertisseur de timestamps et voyez si la date semble plausible. Si une interprétation en « secondes » donne 1970 et une interprétation en « millisecondes » donne une date récente plausible, vous avez identifié l’unité en deux secondes.
Le schéma de bug correspondant : traiter des millisecondes comme des secondes (ou l’inverse). Multipliez inutilement un timestamp en secondes par 1000 et 1785235837 devient 1785235837000 secondes — une date située environ 56 000 ans dans le futur, que la plupart des moteurs d’affichage plafonnent ou bouclent en valeurs absurdes. Divisez un timestamp en millisecondes par 1000 alors qu’il était déjà en secondes et vous obtenez le 21 janvier 1970 — c’est pourquoi tant de systèmes cassés affichent des dates en janvier 1970. Quand vous voyez 1970 en production, suspectez d’abord un décalage d’unité.
Le problème de 2038
Le time_t Unix classique était un entier 32 bits signé comptant des secondes. Sa valeur maximale, 2 147 483 647, correspond au 19 janvier 2038 à 03:14:07 UTC. Une seconde plus tard, un compteur 32 bits signé boucle à −2 147 483 648, ce qui s’affiche comme le 13 décembre 1901 à 20:45:52 UTC. C’est le problème « Y2038 » ou « Epochalypse » — structurellement identique au bug de l’an 2000, mais enraciné dans un type C précis plutôt que dans des champs d’année à deux chiffres.
Où cela compte encore réellement, en 2026 :
- Systèmes embarqués 32 bits et firmware. Appareils à longue durée de vie — contrôleurs industriels, systèmes automobiles, dispositifs médicaux, infrastructures — fonctionnant avec des noyaux 32 bits ou des ABI
time_t32 bits. C’est la principale classe de risque résiduel, précisément parce que ces systèmes sont difficiles à patcher et faciles à oublier. - Vieux formats de fichiers et protocoles ayant spécifié des compteurs de secondes sur 32 bits. Certains sont corrigés en réinterprétant le champ comme non signé (ce qui repousse la limite à 2106) ; d’autres sont simplement legacy.
- Bases de données avec des types limités à l’epoch. L’exemple bien connu : le type
TIMESTAMPde MySQL n’allait historiquement que jusqu’au2038-01-19 03:14:07 UTCcar il stockait les secondes dans un entier 32 bits non signé. (DATETIMEcouvre une plage jusqu’à l’an 9999 et n’est pas affecté.) Si votre schéma utiliseTIMESTAMP, connaissez cette limite.
Ce qui a déjà été corrigé :
- Linux 64 bits et les systèmes 64 bits modernes utilisent un
time_t64 bits, bon pour ~292 milliards d’années. - Le travail
time64du noyau Linux (achevé dans le noyau 5.6, 2020) et le support du temps 64 bits de glibc (_TIME_BITS=64de glibc 2.34) offrent un chemin de migration au userland Linux 32 bits, même si les binaires doivent être recompilés avec la nouvelle ABI — les vieux binaires compilés sur systèmes 32 bits restent vulnérables. - JavaScript, Python, Go et Java utilisent tous des représentations temporelles 64 bits (ou float64) et ne débordent sur aucun horizon humainement pertinent. Les millisecondes en float64 de JavaScript couvrent ~285 000 ans dans chaque direction.
Conseil pratique : si vous écrivez du code nouveau sur une plateforme 64 bits avec un langage courant, 2038 n’est pas votre problème. Si vous maintenez du firmware embarqué, du C legacy sur cibles 32 bits, ou des schémas remplis de colonnes epoch en INT et de champs TIMESTAMP MySQL, auditez maintenant — les systèmes installés dans les années 2020 tourneront encore en 2038.
Les schémas de bugs qui se produisent vraiment
1. Confusion d’unités (secondes vs millisecondes)
Vu plus haut : le plus fréquent et le plus facile à repérer (dates en 1970 ou en l’an 58000). Corrigez par convention : nommez vos variables et champs avec l’unité — created_at_ms, expires_s — et validez aux frontières d’API. Un timestamp hors de, disons, 10^9–10^10 secondes est suspect pour un champ « secondes » en ce siècle.
2. Heure locale prise pour de l’UTC
L’inverse de la règle « les timestamps n’ont pas de fuseau horaire ». Manifestation classique : un serveur à Shanghai et un serveur en Virginie calculent « la même » échéance différemment parce qu’un côté a construit le timestamp à partir de composantes d’heure locale. Tout chemin de code qui fait champs de date/heure locaux → timestamp doit spécifier explicitement le fuseau horaire de ces champs. new Date(2026, 6, 28) en JavaScript signifie minuit dans le fuseau horaire local du navigateur, ce qui est un instant différent pour chaque utilisateur.
3. Les cas limites de l’heure d’été
L’heure d’été (DST) crée deux sortes d’heures locales cassées :
- Heures inexistantes. Dans les fuseaux horaires américains, le 08/03/2026 à 02:30 heure locale n’existe pas — les horloges sautent de 02:00 à 03:00. Un planificateur de type cron réglé sur « exécuter à 02:30 chaque jour » sautera, s’exécutera en double, ou se comportera selon la plateforme ce jour-là.
- Heures ambiguës. Le 01/11/2026, 01:30 heure locale se produit deux fois dans les fuseaux américains lors du retour à l’heure d’hiver. Stocker « 01:30 » sans offset ni fuseau horaire ne permet pas de distinguer laquelle vous vouliez dire.
Les corrections robustes : planifiez les tâches récurrentes en UTC, stockez les instants comme timestamps, et ne convertissez en heure locale qu’à l’affichage. Si vous devez stocker des heures locales futures (par exemple « la réunion de 9 h de l’utilisateur »), stockez l’heure locale plus le nom du fuseau IANA (America/New_York) et résolvez en timestamp au moment de l’affichage — car les gouvernements changent les règles de DST, et un timestamp calculé aujourd’hui pour une réunion locale de 2028 peut être faux après un changement de loi.
4. Les mois indexés à partir de zéro en JavaScript
new Date(2026, 6, 28) correspond au 28 juillet, pas au 28 juin — les mois vont de 0 à 11 tandis que les jours vont de 1 à 31. Cette incohérence a produit une génération de bugs de décalage d’un mois. Atténuations : utilisez Date.UTC(...) quand vous voulez de l’UTC, utilisez des chaînes ISO (new Date("2026-07-28T00:00:00Z")) pour la clarté, ou utilisez une bibliothèque (Temporal, une fois largement déployé, corrige cela proprement avec des mois indexés à partir de 1).
5. Précision des flottants sur les secondes
time.time() en Python renvoie un flottant. Un float64 a 53 bits de mantisse ; aux magnitudes actuelles de l’epoch (~1.79 × 10^9 secondes), la résolution est fine (~240 nanosecondes), donc cela mord rarement en Python — mais le même raisonnement mord dans les systèmes qui transportent des timestamps en microsecondes ou nanosecondes en float64 : une valeur en microsecondes à 16 chiffres exige 54 bits pour être représentée exactement, donc les microsecondes en float64 sont déjà imprécises aujourd’hui. Utilisez des entiers pour tout ce qui est plus fin que la milliseconde.
6. Parsing de chaînes sans offset
new Date("2026-07-28") en JavaScript est parsé comme minuit UTC, mais new Date("2026-07-28 00:00:00") (sans T, sans Z) est parsé comme minuit local — un basculement de fuseau silencieux fondé sur des détails de format de chaîne. Incluez toujours un Z explicite ou un offset numérique dans les dates-heures sérialisées. Préférez l’ISO 8601 avec offset : 2026-07-28T10:00:00+08:00.
Obtenir le timestamp correctement, langage par langage
JavaScript / TypeScript — Date.now() renvoie des millisecondes ; divisez pour des secondes :
const ms = Date.now(); // 1785235837123
const seconds = Math.floor(Date.now() / 1000); // 1785235837
// From a specific instant, always specify UTC explicitly:
const ts = Date.UTC(2026, 6, 28, 0, 0, 0) / 1000; // months 0-indexed!
Python — time.time() renvoie des secondes en flottant ; utilisez datetime avec UTC explicite pour tout ce que vous stockerez :
import time
from datetime import datetime, timezone
seconds = int(time.time()) # 1785235837
ns = time.time_ns() # nanoseconds, exact integer
ts = int(datetime.now(timezone.utc).timestamp())
Go — le package time vous donne chaque unité explicitement :
now := time.Now()
seconds := now.Unix() // int64 seconds
millis := now.UnixMilli() // int64 milliseconds
nanos := now.UnixNano() // int64 nanoseconds — mind int64 range
Java — évitez les legacy Date/Calendar ; utilisez java.time :
long millis = System.currentTimeMillis();
long seconds = Instant.now().getEpochSecond();
long nanos = Instant.now().getNano(); // nano-of-second, not epoch nanos!
Bash — GNU et macOS (BSD) date diffèrent sur le parsing :
date +%s # current epoch seconds (both)
date -d @1785235837 # GNU: timestamp → human date
date -r 1785235837 # macOS/BSD: timestamp → human date
date -u -d "2026-07-28 00:00:00 UTC" +%s # GNU: date → timestamp
date -u -j -f "%Y-%m-%d %H:%M:%S" "2026-07-28 00:00:00" +%s # macOS
Notez le thème récurrent : chaque API correcte vous donne soit directement la valeur d’epoch, soit vous force à nommer le fuseau horaire. Chaque API dangereuse vous permet de l’omettre.
Travailler avec des timestamps de façon interactive
Quand vous déboguez un timestamp issu d’un log ou d’une réponse d’API, ne faites pas le calcul de tête. Collez-le dans notre convertisseur de timestamps : il affiche l’instant en UTC et dans votre fuseau horaire local, et détecte automatiquement secondes vs millisecondes — exactement le contrôle d’ambiguïté décrit plus haut. Il convertit aussi dans l’autre sens (date humaine + fuseau horaire → timestamp) pour construire des jeux d’essai.
Résumé
- Un timestamp Unix est un nombre de secondes (ou ms/µs/ns) depuis le 01/01/1970 UTC. Il n’a ni fuseau horaire ni secondes intercalaires.
- Le nombre de chiffres identifie l’unité à l’ère moderne : 10 = secondes, 13 = millisecondes, 16 = microsecondes, 19 = nanosecondes.
- 2038 fait déborder le
time_t32 bits signé le 2038-01-19 03:14:07 UTC. Les systèmes 64 bits et les langages courants sont à l’abri ; le firmware embarqué, les binaires 32 bits legacy et les colonnesTIMESTAMPMySQL sont le risque résiduel. - Les schémas de bugs réels : confusion d’unités, erreurs local vs UTC, cas limites de DST, mois indexés à zéro en JS, perte de précision float64 sur les µs/ns, et parsing de chaînes sans offset.
- Stockez les instants comme timestamps entiers, nommez les champs avec leurs unités, planifiez en UTC, ne convertissez en local qu’à l’affichage — et gardez un convertisseur de timestamps en favori pour le débogage qui passera quand même entre les mailles.