zylior
← Blog

Sendefenster pro Zeitzone: das Postfach zur richtigen Zeit treffen

"Wir senden um 9 Uhr" — gut, aber wessen 9 Uhr? Wenn deine Liste Abonnenten in Paris, Montreal und Singapur hat, trifft ein einziger Versand "um 9 Uhr" für eine Zeitzone die 9 Uhr und für eine andere 3 Uhr nachts. So berechnest du das Sendefenster in der echten Zeitzone jeder Person, ohne dass dich die Sommerzeit reinlegt.

Das Problem: "9 Uhr" ist keine Uhrzeit, sondern eine Zeitzone

Wenn du eine Kampagne "um 9 Uhr" planst, interpretiert dein Tool das in seiner Zeitzone (oft UTC, manchmal die deines Coolify-Servers, manchmal deine persönliche). Für einen Abonnenten in `America/Los_Angeles` landet ein auf 9 Uhr `Europe/Paris` festgelegter Versand bei ihm um Mitternacht. Leg ihn im Winter auf 9 Uhr UTC, und es ist 1 Uhr nachts in Paris, 4 Uhr in Dubai.

Das richtige Ziel ist nicht "zum Zeitpunkt T senden", sondern "die E-Mail zwischen 8 und 10 Uhr Ortszeit jedes Abonnenten ankommen lassen". Das bedeutet einen pro Zeitzone unterschiedlichen Sendezeitpunkt, rückwärts ab der gewünschten Ortszeit berechnet.

Der Unterschied bei der Öffnungsrate zwischen "gutem Zeitfenster" und "3 Uhr nachts" ist nicht marginal. Eine nachts zugestellte E-Mail liegt beim Aufwachen unter 20 anderen begraben, und je mehr ungeöffnete Sendungen du anhäufst, desto stärker verschlechtert sich deine Absenderreputation (Gmail/Yahoo lesen das Engagement). Das Timing schützt die Zustellbarkeit, nicht nur die Öffnungsrate des Tages.

Das Fenster berechnen: zuerst die Ortszeit, dann der UTC-Zeitpunkt

Die goldene Regel: Du speicherst und vergleichst immer in UTC, aber du denkst in Ortszeit. Die korrekte Pipeline ist: (1) Du gehst von der gewünschten Ortszeit aus, z. B. 9 Uhr; (2) du nimmst die IANA-Zeitzone des Abonnenten, z. B. `Europe/Paris`; (3) du wandelst "9 Uhr Ortszeit heute" in einen UTC-Zeitpunkt um; (4) du schiebst diesen Zeitpunkt in deine Sende-Queue.

Die tödliche Falle ist, den Versatz einzufrieren. `Europe/Paris` ist NICHT "UTC+1": es ist UTC+1 im Winter und UTC+2 im Sommer (Sommerzeit / DST). Wenn du `+1` hardcodest, sendest du das halbe Jahr eine Stunde zu früh. Der einzig korrekte Weg ist, über den IANA-Zeitzonennamen zu gehen und die Bibliothek die DST-Umstellung am betreffenden Datum erledigen zu lassen.

// Node 18+, zéro dépendance : Intl gère IANA + DST nativement
// On veut : instant UTC où il sera `targetHour` heure locale chez l'abonné
function sendInstantUTC(tz, targetHour, baseDate = new Date()) {
  // Heure locale actuelle dans tz (gère DST automatiquement)
  const fmt = new Intl.DateTimeFormat('en-US', {
    timeZone: tz, hour12: false,
    year: 'numeric', month: '2-digit', day: '2-digit',
    hour: '2-digit', minute: '2-digit', second: '2-digit',
  });
  const p = Object.fromEntries(
    fmt.formatToParts(baseDate).filter(x => x.type !== 'literal')
       .map(x => [x.type, x.value]));
  // décalage tz = (heure murale locale) - (heure UTC) à cet instant
  const asUTC = Date.UTC(+p.year, +p.month - 1, +p.day,
                         +p.hour, +p.minute, +p.second);
  const offsetMs = asUTC - baseDate.getTime();
  // instant UTC pour targetHour:00 heure locale, le jour local courant
  const localTargetUTC = Date.UTC(+p.year, +p.month - 1, +p.day, targetHour, 0, 0);
  return new Date(localTargetUTC - offsetMs);
}

sendInstantUTC('Europe/Paris', 9);        // 9h Paris -> ~07:00Z (été) / 08:00Z (hiver)
sendInstantUTC('America/Los_Angeles', 9); // 9h LA -> ~16:00Z (été) / 17:00Z (hiver)
Erfinde die DST-Berechnung nicht von Hand neu. `Intl.DateTimeFormat` mit `timeZone` (Node, Deno, Browser) oder eine Bibliothek wie `luxon`/`Temporal` kennt die IANA-Datenbank und wechselt Sommer-/Winterzeit am richtigen Datum. Der selbstgebaute "fester Versatz"-Code ist DER Bug, der durchs Review rutscht und zweimal im Jahr kaputtgeht.

Den Versand strecken: lass nicht eine ganze Zeitzone auf einmal raus

Sobald die Zeitpunkte berechnet sind, hast du Pakete: alle deine `Europe/Paris`-Abonnenten um 07:00Z, alle deine `America/New_York` um 13:00Z usw. Wenn jedes Paket in einem Schwall rausgeht, gehst du zwei Risiken ein: Throttling auf Anbieterseite (Resend, SES) und eine Spitze an Öffnungen/Abmeldungen, die wie Spam aussieht. Strecke jedes Fenster.

Die Falle fehlender oder eingefrorener Zeitzonen

Du wirst nie die Zeitzone von 100 % deiner Liste haben. Zwei schlechte Reflexe: (1) deine eigene Zeitzone auf alle anwenden — du sendest der halben Welt um 3 Uhr nachts; (2) die Zeitzone bei der Anmeldung einfrieren und nie aktualisieren — jemand zieht um, reist, oder du hast sie falsch erkannt.

Saubere Rückfallstrategie, nach Verlässlichkeit geordnet: vom Abonnenten angegebene Zeitzone > aus jüngster Aktivität abgeleitete Zeitzone (häufigster Öffnungs-/Klick-Zeitstempel) > aus dem Land abgeleitete Zeitzone (Anmeldefeld, Signup-IP). Als allerletzter Ausweg ein expliziter Standardwert — typischerweise die Zeitzone des Großteils deiner Liste — und nie vor 8 Uhr und nie nach 21 Uhr in diesem Standardwert. Lieber eine E-Mail um 8 Uhr für eine ungefähre Zeitzone als eine garantiert um 3 Uhr nachts.

Speichere den IANA-Namen (`Europe/Paris`), nie den Versatz (`+02:00`) und keine Abkürzung (`CET`, mehrdeutig und nicht DST-bewusst). Der Versatz wird bei der nächsten DST-Umstellung falsch; der IANA-Name bleibt für immer korrekt, weil die Bibliothek den Versatz zum Sendezeitpunkt neu berechnet.

Um morgen anzufangen: füge deinen Abonnenten ein IANA-`timezone`-Feld hinzu, bring deinen Scheduler von "einziger UTC-Zeitpunkt" auf "gewünschte Ortszeit + Umrechnung pro Zeitzone", setze das Fenster auf 8–10 Uhr Ortszeit mit Jitter, und logge die verwendete Zeitzone + ihre Quelle (angegeben/abgeleitet/Standard) für jeden Versand. Du wirst die Öffnungen in den Zeitzonen steigen sehen, die du unwissentlich abgewürgt hast — und du hast die Spur, um zu beweisen, dass es kein Zufall ist.

Der Newsletter

Mit der Anmeldung stimmst du dem Erhalt des Zylior-Newsletters zu. 1-Klick-Abmeldung in jeder E-Mail.