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.
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)
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.
- Fenster, nicht Zeitpunkt: ziele auf 8–10 Uhr Ortszeit, nicht punkt 9 Uhr. Das gibt dir 120 Minuten zum Verteilen.
- Jitter pro Abonnent: füge einen stabilen Zufallsversatz hinzu, z. B. `hash(email) % 90` Minuten, um zu glätten, ohne bei jedem Lauf neu zu sortieren.
- Durchsatzlimit: deckle auf das, was dein Anbieter toleriert (Resend free ~2 req/s, SES je nach Kontingent). Die Queue arbeitet mit konstantem Durchsatz ab.
- Idempotenz: ein `Kampagne+Abonnent`-Schlüssel, damit ein Retry nie zweimal sendet — ein Worker-Crash mitten in einem Paket darf die Sendungen nicht verdoppeln.
- Vermeide 0–6 Uhr Ortszeit: hat ein Abonnent keine bekannte Zeitzone, rate NICHT — siehe nächster Abschnitt.
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.
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.