Zum Hauptinhalt springen
Anleitung

Unix-Zeitstempel in der Produktion: Sekunden vs. Millisekunden, das 2038-Problem und Zeitzonen-Bugs

· 12 min Lesezeit

Jedes Produktionssystem loggt, speichert oder überträgt irgendwann einen Unix-Zeitstempel. Und jedes Produktionssystem liefert irgendwann einen Bug, der einen enthält — ein Datum, das als Januar 1970 angezeigt wird, ein 49 Jahre in der Vergangenheit geplantes Ereignis, eine API-Integration, bei der eine Seite Millisekunden erwartet und die andere Sekunden sendet. Diese Bugs sind langweilig, häufig und vollständig vermeidbar, sobald man die Handvoll Regeln kennt, die bestimmen, wie Zeitstempel tatsächlich funktionieren.

Dieser Leitfaden behandelt den praktischen Kern: das Erkennen von Zeitstempel-Einheiten anhand der Stellenzahl, das 2038-Problem und seinen heutigen tatsächlichen Status, die Bug-Muster, die tatsächlich auftreten, und den korrekten Weg, einen Zeitstempel in den Sprachen zu bekommen, die du wahrscheinlich nutzt.

Was ein Unix-Zeitstempel ist (und nicht ist)

Ein Unix-Zeitstempel zählt die Sekunden seit 1970-01-01 00:00:00 UTC — der „Unix-Epoche”. Zwei Eigenschaften sind für alles Folgende wichtig:

  1. Er hat keine Zeitzone. Der Zeitstempel 1785235837 bezeichnet überall auf der Erde gleichzeitig einen spezifischen Augenblick. Zeitzonen kommen nur ins Spiel, wenn du einen Zeitstempel einem Menschen anzeigst oder einen aus lokalen Wanduhr-Komponenten konstruierst. Wenn du einen Satz aus diesem Artikel verinnerlichst, dann diesen: Zeitstempel sind UTC-Instante; Zeitzonen sind eine Render-Frage.
  2. Er ignoriert Schaltsekunden. Die Unix-Zeit nimmt an, dass jeder Tag genau 86.400 Sekunden hat. Wenn eine Schaltsekunde eingefügt wird, wiederholt POSIX die effektiv oder verschmiert sie (Verhalten variiert je nach System; Googles „Leap Smear” verteilt sie über den Tag). Für die meisten Anwendungen ist das irrelevant; für Intervall-Berechnungen, die eine Schaltsekunde umspannen, bedeutet es, dass deine „exakt N Sekunden”-Annahme um eins daneben liegen kann.

Beachte auch, dass die Epochen-Wahl bedeutet, dass negative Zeitstempel gültig sind: -1 ist 1969-12-31 23:59:59 UTC. Code, der negative Zeitstempel als Fehler behandelt, bricht bei Daten vor 1970 — ein echtes Bug-Muster für Systeme, die historische Daten verarbeiten (Geburtsdaten, Archiv-Aufzeichnungen).

Sekunden, Millisekunden, Mikrosekunden: die Stellenzahl-Heuristik

Zeitstempel kommen aus verschiedenen Ökosystemen in verschiedenen Einheiten, und die Einheit falsch zu identifizieren ist der häufigste Zeitstempel-Bug. Der schnelle Feldtest ist das Zählen der Stellen:

EinheitBeispiel (Mitte 2026)StellenWann diese Breite erreicht wird
Sekunden17852358371010 Stellen seit 2001-09-09; 11 Stellen ab 2286
Millisekunden17852358370001313 Stellen seit 2001-09-09
Mikrosekunden17852358370000001616 Stellen
Nanosekunden178523583700000000019überschreitet int64-Bereich 2262

Faustregeln für die heutige Ära (2001–2286):

  • 10 Stellen → Sekunden. Immer sicher anzunehmen.
  • 13 Stellen → Millisekunden. JavaScripts Date.now(), Javas System.currentTimeMillis() und die meisten Logging-/Metrik-Systeme geben diese aus.
  • 16 Stellen → Mikrosekunden. Pythons time.time() × 1.000.000, einige Datenbanken, einige Tracing-Systeme.
  • 19 Stellen → Nanosekunden. Gos time.Now().UnixNano(), und das ist eine Falle: 19 Stellen überschreiten das Maximum signed 64-Bit (9.223.372.036.854.775.807) nach 2262, aber heute passt eine 19-stellige Zahl gerade noch in int64. Wenn du Nanosekunden-Zeitstempel speicherst, mach keine Arithmetik, die Headroom voraussetzt.

Eine schnelle Plausibilitätsprüfung beim Debuggen: füge die Zahl in einen Zeitstempel-Konverter ein und schau, ob das Datum plausibel aussieht. Wenn eine „Sekunden”-Interpretation 1970 ergibt und eine „Millisekunden”-Interpretation ein plausibles aktuelles Datum, hast du die Einheit in zwei Sekunden identifiziert.

Das zugehörige Bug-Muster: Millisekunden als Sekunden behandeln (oder umgekehrt). Multipliziere einen Sekunden-Zeitstempel unnötig mit 1000, und 1785235837 wird zu 1785235837000 Sekunden — ein Datum ungefähr 56.000 Jahre in der Zukunft, das die meisten Renderer kappen oder in Müll verwandeln. Teile einen Millisekunden-Zeitstempel durch 1000, wenn er bereits Sekunden waren, und du bekommst 1970-01-21 — weshalb so viele kaputte Systeme Daten im Januar 1970 anzeigen. Wenn du 1970 in der Produktion siehst, verdächtige zuerst einen Einheiten-Mismatch.

Das 2038-Problem

Klassisches Unix-time_t war ein signed 32-Bit-Integer, das Sekunden zählte. Sein Maximalwert, 2.147.483.647, entspricht 2038-01-19 03:14:07 UTC. Eine Sekunde später wickelt sich ein signed 32-Bit-Zähler auf −2.147.483.648, was als 1901-12-13 20:45:52 UTC gerendert wird. Das ist das „Y2038”- oder „Epochalypse”-Problem — strukturell identisch zu Y2K, aber in einem bestimmten C-Typ statt in zweistelligen Jahresfeldern verwurzelt.

Wo es tatsächlich 2026 noch zählt:

  • 32-Bit-Embedded-Systeme und Firmware. Geräte mit langen Lebensdauern — industrielle Steuerungen, Automotive-Systeme, Medizingeräte, Infrastruktur — die 32-Bit-Kernels oder 32-Bit-time_t-ABIs nutzen. Das ist die wichtigste verbleibende Risiko-Klasse, gerade weil diese Systeme schwer zu patchen und leicht zu vergessen sind.
  • Alte Datei-Formate und Protokolle, die 32-Bit-Sekunden-Zähler spezifizierten. Einige werden durch Neuinterpretation des Feldes als unsigned behoben (was das Limit auf 2106 verschiebt); andere sind einfach Legacy.
  • Datenbanken mit epochenbegrenzten Typen. Das bekannte Beispiel: MySQLs TIMESTAMP-Typ reichte historisch nur bis 2038-01-19 03:14:07 UTC, weil er Sekunden als unsigned 32-Bit-Integer speicherte. (DATETIME hat einen Bereich bis Jahr 9999 und ist nicht betroffen.) Wenn dein Schema TIMESTAMP nutzt, kenne dieses Limit.

Was bereits behoben ist:

  • 64-Bit-Linux und moderne 64-Bit-Systeme nutzen ein 64-Bit-time_t, gut für ~292 Milliarden Jahre.
  • Die time64-Arbeit des Linux-Kernels (abgeschlossen in Kernel 5.6, 2020) und glibcs 64-Bit-Zeit-Unterstützung (glibc 2.34s _TIME_BITS=64) geben 32-Bit-Linux-Userland einen Migrationspfad, allerdings müssen Binaries mit der neuen ABI neu gebaut werden — alte kompilierte Binaries auf 32-Bit-Systemen bleiben verwundbar.
  • JavaScript, Python, Go, Java nutzen alle 64-Bit- (oder float64-) Zeit-Repräsentationen und überlaufen in keinem menschenrelevanten Zeitrahmen. JavaScripts float64-Millisekunden gehen ~285.000 Jahre in jede Richtung.

Praktischer Rat: Wenn du neuen Code auf einer 64-Bit-Plattform mit einer Mainstream-Sprache schreibst, ist 2038 nicht dein Problem. Wenn du Embedded-Firmware, Legacy-C auf 32-Bit-Targets oder Schemas voller INT-Epochen-Spalten und MySQL-TIMESTAMP-Felder wartest, auditiere jetzt — in den 2020ern installierte Systeme werden 2038 noch laufen.

Die Bug-Muster, die tatsächlich auftreten

1. Einheiten-Verwirrung (Sekunden vs. Millisekunden)

Oben behandelt: das häufigste und das am leichtesten erkennbare (Daten in 1970 oder im Jahr 58000). Fix per Konvention: benenne deine Variablen und Felder mit der Einheit — created_at_ms, expires_s — und validiere an API-Grenzen. Ein Zeitstempel außerhalb von z. B. 10^910^10 Sekunden ist in diesem Jahrhundert für ein „Sekunden”-Feld verdächtig.

2. Lokale Zeit mit UTC verwechselt

Das Inverse der Regel „Zeitstempel haben keine Zeitzone”. Klassische Manifestation: Ein Server in Shanghai und ein Server in Virginia berechnen „denselben” Termin unterschiedlich, weil eine Seite den Zeitstempel aus lokalen Wanduhr-Komponenten konstruiert. Jeder Codepfad, der lokale Datums-/Zeitfelder → Zeitstempel geht, muss die Zeitzone dieser Felder explizit angeben. new Date(2026, 6, 28) in JavaScript bedeutet Mitternacht in der lokalen Zeitzone des Browsers, was für jeden Nutzer ein anderer Augenblick ist.

3. Daylight-Saving-Time-Kanten

DST erzeugt zwei Arten kaputter Wanduhr-Zeiten:

  • Nicht existierende Zeiten. In US-Zeitzonen existiert 2026-03-08 02:30 lokal nicht — Uhren springen 02:00 → 03:00. Ein Cron-artiger Scheduler, der auf „jeden Tag um 02:30 ausführen” steht, wird an diesem Tag entweder überspringen, doppelt laufen oder plattformabhängig agieren.
  • Mehrdeutige Zeiten. Am 2026-11-01 passiert 01:30 lokal zweimal in US-Zeitzonen, wenn die Uhren zurückgestellt werden. „01:30” ohne Offset oder Zeitzone zu speichern kann nicht unterscheiden, welche gemeint war.

Die robusten Fixes: Plane wiederkehrende Jobs in UTC, speichere Instante als Zeitstempel und konvertiere nur zur Anzeige in lokale Zeit. Wenn du zukünftige lokale Wanduhr-Zeiten speichern musst (z. B. „9-Uhr-Meeting des Nutzers”), speichere die lokale Zeit plus den IANA-Zeitzonen-Namen (America/New_York) und löse zur Render-Zeit zu einem Zeitstempel auf — weil Regierungen DST-Regeln ändern, und ein heute für ein 2028er Meeting berechneter Zeitstempel nach einer Gesetzesänderung falsch sein kann.

4. JavaScripts null-indizierte Monate

new Date(2026, 6, 28) ist Juli 28, nicht Juni 28 — Monate laufen 0–11, Tage 1–31. Diese Inkonsistenz hat eine Generation von Off-by-one-Monats-Bugs erzeugt. Mitigations: nutze Date.UTC(...), wenn du UTC meinst, nutze ISO-Strings (new Date("2026-07-28T00:00:00Z")) für Klarheit, oder nutze eine Bibliothek (Temporal, sobald es breit verfügbar ist, fixt das ordentlich mit 1-indizierten Monaten).

5. Float-Präzision bei Sekunden

Pythons time.time() gibt einen Float zurück. Ein float64 hat 53 Bits Mantisse; bei aktuellen Epochen-Größen (~1.79 × 10^9 Sekunden) ist die Auflösung fein (~240 Nanosekunden), also beißt das in Python selten — aber dieselbe Überlegung beißt in Systemen, die Mikro- oder Nanosekunden-Zeitstempel als float64 tragen: ein 16-stelliger Mikrosekunden-Wert braucht 54 Bits zur exakten Darstellung, also sind float64-Mikrosekunden bereits heute ungenau. Nutze Integer für alles feiner als Millisekunden.

6. String-Parsing ohne Offset

new Date("2026-07-28") in JavaScript parst als UTC-Mitternacht, aber new Date("2026-07-28 00:00:00") (kein T, kein Z) parst als lokale Mitternacht — ein lautloser Zeitzonen-Flip basierend auf String-Format-Details. Füge immer ein explizites Z oder numerisches Offset in serialisierten Datetimes ein. Bevorzuge ISO 8601 mit Offset: 2026-07-28T10:00:00+08:00.

Zeitstempel korrekt bekommen, pro Sprache

JavaScript / TypeScriptDate.now() gibt Millisekunden zurück; teile für Sekunden:

const ms = Date.now();                      // 1785235837123
const seconds = Math.floor(Date.now() / 1000); // 1785235837
// Für einen bestimmten Augenblick, immer UTC explizit angeben:
const ts = Date.UTC(2026, 6, 28, 0, 0, 0) / 1000; // Monate 0-indiziert!

Pythontime.time() gibt Float-Sekunden zurück; nutze datetime mit explizitem UTC für alles, was du speicherst:

import time
from datetime import datetime, timezone

seconds = int(time.time())                    # 1785235837
ns = time.time_ns()                           # Nanosekunden, exakter Integer
ts = int(datetime.now(timezone.utc).timestamp())

Go — das time-Paket gibt dir jede Einheit explizit:

now := time.Now()
seconds := now.Unix()       // int64 Sekunden
millis  := now.UnixMilli()  // int64 Millisekunden
nanos   := now.UnixNano()   // int64 Nanosekunden — int64-Bereich beachten

Java — vermeide Legacy Date/Calendar; nutze java.time:

long millis  = System.currentTimeMillis();
long seconds = Instant.now().getEpochSecond();
long nanos   = Instant.now().getNano(); // Nano-of-Second, nicht Epochen-Nanos!

Bash — GNU vs. macOS (BSD) date unterscheiden sich beim Parsen:

date +%s                        # aktuelle Epochen-Sekunden (beide)
date -d @1785235837             # GNU: Zeitstempel → menschliches Datum
date -r 1785235837              # macOS/BSD: Zeitstempel → menschliches Datum
date -u -d "2026-07-28 00:00:00 UTC" +%s   # GNU: Datum → Zeitstempel
date -u -j -f "%Y-%m-%d %H:%M:%S" "2026-07-28 00:00:00" +%s  # macOS

Beachte das wiederkehrende Thema: jede korrekte API gibt dir entweder den Epochen-Wert direkt oder zwingt dich, die Zeitzone zu benennen. Jede gefährliche API lässt dich sie weglassen.

Interaktiv mit Zeitstempeln arbeiten

Wenn du einen Zeitstempel aus einem Log oder einer API-Antwort debuggst, rechne nicht im Kopf. Füge ihn in unseren Zeitstempel-Konverter ein: Er zeigt den Augenblick in UTC und deiner lokalen Zeitzone und erkennt automatisch Sekunden vs. Millisekunden — genau die oben beschriebene Mehrdeutigkeitsprüfung. Er konvertiert auch in die andere Richtung (menschliches Datum + Zeitzone → Zeitstempel) zum Erstellen von Test-Fixtures.

Zusammenfassung

  • Ein Unix-Zeitstempel ist Sekunden (oder ms/µs/ns) seit 1970-01-01 UTC. Er hat keine Zeitzone und keine Schaltsekunden.
  • Die Stellenzahl identifiziert die Einheit in der modernen Ära: 10 = Sekunden, 13 = Millisekunden, 16 = Mikrosekunden, 19 = Nanosekunden.
  • 2038 überläuft signed 32-Bit time_t um 2038-01-19 03:14:07 UTC. 64-Bit-Systeme und Mainstream-Sprachen sind sicher; Embedded-Firmware, Legacy-32-Bit-Binaries und MySQL-TIMESTAMP-Spalten sind das verbleibende Risiko.
  • Die realen Bug-Muster: Einheiten-Verwirrung, lokal-vs.-UTC-Fehler, DST-Kanten, JS-null-indizierte Monate, float64-Präzisionsverlust bei µs/ns und String-Parsing ohne Offset.
  • Speichere Instante als Integer-Zeitstempel, benenne Felder mit Einheiten, plane in UTC, konvertiere nur zur Anzeige in lokal — und hab einen Zeitstempel-Konverter als Lesezeichen für das Debugging, das trotzdem durchrutscht.