Suche

↑↓ auswählenEnter öffnenErweiterte Suche

Anleitungen

Wo steht deine Datenbank? Warum die D1-Region beim Anlegen über die Ladezeit entscheidet

Lässt du D1 und R2 beim ersten Deploy von Wrangler anlegen, sucht Cloudflare den Standort aus, und ändern kannst du ihn danach nicht mehr. Steht die Datenbank in Nordamerika, kostet jede ungecachte Seite rund 140 ms pro Abfrage. Leg beides deshalb vorher selbst an, mit --location weur. Ist es schon zu spät, hilft Read Replication beim Lesen. Reparieren kann es nur ein Umzug.

Von Publiziert am 11 Min. Lesezeitgeprüft mit 1.1 · aktuell 1.2

Kevins Einstieg in EmDash bringt dich vom leeren Ordner zur laufenden Site auf Cloudflare. Eine Entscheidung trifft dabei Wrangler für dich, fast nebenbei: Beim ersten Deploy legt es Datenbank und Media-Bucket ohne Standortangabe an. Wo die beiden landen, bestimmt Cloudflare, und ändern lässt sich das danach nicht mehr. Stehen sie weit weg von deinen Lesern, zahlt jede ungecachte Seite bei jeder Abfrage für die Entfernung.

Hier geht es um genau diesen Schritt: wie du D1 und R2 vor dem ersten Deploy selbst in der richtigen Region anlegst, und was du tust, wenn es dafür schon zu spät ist. Die Installation steht bei Kevin. Geprüft habe ich die Befehle mit Wrangler 4.147.0 und dem Blog-Template von EmDash 1.1.0.

Was beim ersten Deploy passiert

Im Template stehen Datenbank und Bucket in wrangler.jsonc nur mit Namen, ohne ID:

"d1_databases": [{ "binding": "DB", "database_name": "my-emdash-site" }],
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "my-emdash-media" }],

Findet Wrangler beim Deploy unter diesem Namen nichts, legt es die Ressource an und meldet Creating new D1 Database "my-emdash-site" beziehungsweise Creating new R2 Bucket "my-emdash-media". Laut Quelltext von Wrangler 4.147.0 geht dabei nur der Name an die API, ein Location Hint fehlt. Nachtragen kannst du ihn auch nicht, denn wrangler.jsonc hat für D1 und R2 kein Feld für den Standort.

Ohne Hinweis legt Cloudflare die Datenbank laut Doku in der Nähe des Ortes an, von dem die Anfrage kommt, und bei R2 ist es genauso. Deployst du vom Laptop in München, landet sie meistens in Europa. Läuft der erste Deploy aber in einer CI-Pipeline oder über einen VPN-Ausgang, sieht Cloudflare einen anderen Ort, und genau diesen Fall nennt die D1-Doku als Grund für einen Location Hint. Uns ist das schon passiert: Eine Datenbank und ein Bucket, beide ohne Hinweis angelegt, landeten in WNAM, also Western North America, für Sites mit Lesern in Deutschland. Eine andere Datenbank kam in Osteuropa (EEUR) an statt in Westeuropa. Warum genau, haben wir damals nicht festgehalten.

Was dich die falsche Region kostet

Das klingt nach einer Formalität. Teuer wird es, weil EmDash für jede Seite, die nicht aus dem Edge-Cache kommt, mehrmals nacheinander die Datenbank fragt, und jede Abfrage kostet einmal den Weg hin und zurück. Im August 2026 haben wir auf zwei Sites mit EmDash 0.3x drei bis sechs Abfragen pro Seite gezählt, wenn der Worker warm war, und zwölf bis sechzehn nach einem Kaltstart, weil EmDash dann erst Plugin-Status, Site-Einstellungen und Hooks lädt.

Von München aus dauerte eine Abfrage an eine Datenbank in WNAM rund 140 ms, an eine in Osteuropa (EEUR) 20 bis 25 ms. Mit der Datenbank in WNAM brauchte derselbe Code nach einem Kaltstart 2,1 bis 2,9 Sekunden für eine Seite, in EEUR 0,85 bis 1,2 Sekunden. Ein Cache-Treffer lag in beiden Fällen bei 30 bis 40 ms.

Der Edge-Cache versteckt das Problem also nur, er löst es nicht. Du zahlst bei jedem Cache-Miss, nach jedem Speichern, das den Cache leert, und im Adminbereich sowieso immer, denn der wird nie gecacht. Deshalb merkt deine Redaktion die falsche Region als Erste.

Wo deine Ressourcen stehen, verrät dir Wrangler:

pnpm wrangler d1 info my-emdash-site          # Zeile running_in_region
pnpm wrangler r2 bucket info my-emdash-media  # Zeile location

Was eine einzelne Seite kostet, zeigt der server-timing-Header von EmDash, samt Zahl der Abfragen. Weil er mitgecacht wird, hängst du beim Messen einen Query-Parameter an, der sich bei jedem Aufruf ändert. Dann bekommst du einen Cache-Miss.

Vor dem ersten Deploy ersparst du dir das alles mit zwei Befehlen.

D1 und R2 selbst anlegen, bevor du deployst

Zuerst die Namen. Benenne Worker, Datenbank und Bucket in wrangler.jsonc nach deinem Projekt. Wrangler findet Ressourcen nämlich über den Namen. Lässt du es bei my-emdash-site, verbindet sich dein zweites EmDash-Projekt im selben Konto mit der Datenbank des ersten.

"name": "mein-blog",
"d1_databases": [{ "binding": "DB", "database_name": "mein-blog" }],
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "mein-blog-media" }],

Dann mit Standort anlegen. Vorher meldest du dich mit pnpm wrangler login an.

pnpm wrangler d1 create mein-blog --location weur --binding DB --update-config
pnpm wrangler r2 bucket create mein-blog-media --location weur --binding MEDIA --update-config

Mit --binding DB --update-config schreibt Wrangler die neue database_id in den vorhandenen Eintrag mit dem Binding DB, statt einen zweiten danebenzulegen. Für --location gibt es die Werte weur, eeur, wnam, enam, apac und oc. Sitzen deine Leser in Deutschland, Österreich oder der Schweiz, nimm weur.

Dann nachsehen. Ein Location Hint ist ein Wunsch, keine Garantie. Schau wie oben mit d1 info und r2 bucket info nach. Erst wenn beides stimmt, deployst du wie in Kevins Artikel. Steht beim Deploy trotzdem Creating new in der Ausgabe, passt ein Name nicht.

Location Hint oder Jurisdiction?

Beide Befehle kennen auch --jurisdiction. Das klingt ähnlich, meint aber etwas anderes. Ein Location Hint ist eine Performance-Entscheidung, die Cloudflare nach bestem Bemühen umsetzt. Eine Jurisdiction wie eu ist dagegen eine Garantie, dass die Daten in der EU gespeichert und verarbeitet werden. Setzen lassen sich beide nur beim Anlegen.

Gibst du beides an, gewinnt bei D1 die Jurisdiction, und Read Replicas entstehen nur innerhalb der Jurisdiction. Bei R2 lehnt Wrangler die Kombination ab. Ein R2-Bucket mit Jurisdiction braucht außerdem "jurisdiction": "eu" im Binding, und genau das schreibt --update-config in Wrangler 4.147.0 nicht mit. Fehlt der Eintrag, findet Wrangler den Bucket beim Deploy nicht und legt einen neuen an, wieder ohne Standort.

Für einen Blog brauchst du das alles nicht, da reicht --location weur.

Schon deployt? Read Replication oder Placement

Steht deine Datenbank schon in der falschen Region, musst du nicht sofort umziehen. Zwei Funktionen lindern das Problem, beheben es aber nicht.

Read Replication legt ohne Aufpreis in jeder D1-Region eine Lesekopie an. Steht die Primärdatenbank in WNAM, lesen europäische Besucher dann aus Europa. Dafür brauchst du zwei Schalter. Den einen legst du auf der Datenbank um, im Dashboard oder per REST-API, denn Wrangler kann das nicht. Den anderen, session: "auto" in EmDashs d1(), setzt das Blog-Template schon.

Jeder Schreibzugriff geht aber weiterhin an die Primärdatenbank, und schreiben tut vor allem deine Redaktion. Außerdem gibt es eine Falle: Das Compatibility Flag global_fetch_strictly_public blockiert die internen Anfragen, über die D1 die Kopien anspricht (emdash#1273). Früher hing dann jede Seite, ohne Fehlermeldung. Seit EmDash 0.34 bricht EmDash eine solche Abfrage nach fünf Sekunden ab, schreibt einen Fehler ins Log und liest ohne Replikation weiter. Die Seiten laden also, aber in jedem neuen Isolate wartet der erste Aufruf fünf Sekunden, und von der Replikation hast du nichts. Trotzdem würde ich bei einer Datenbank in der falschen Region zuerst Read Replication einschalten und neu messen. Oft erspart dir das den Umzug.

Placement verlegt statt der Daten den Worker. Normalerweise läuft er im Rechenzentrum beim Leser. Mit einem Placement Hint läuft er in der Nähe seines Backends, die vielen Abfragen werden kurz, dafür reist jede ungecachte Anfrage einmal über den Atlantik und zurück. Das ist besser als zwölfmal, aber keine europäische Ladezeit. Immerhin profitiert auch deine Redaktion, denn ihre Schreibzugriffe werden ebenfalls kurz. Eine D1-Datenbank kannst du laut Doku nicht direkt als Ziel angeben, nur eine Region von AWS, GCP oder Azure. Du wählst also eine in der Nähe deiner Primärdatenbank. Die EmDash-Doku empfiehlt genau diesen Weg und rät dann von Read Replication ab: session bleibt auf "disabled", damit Lesen und Schreiben die nahe Primärdatenbank treffen.

Smart Placement, die automatische Variante, hilft dir hier dagegen kaum. Sie wählt laut Doku nur Rechenzentren, in denen dein Worker schon gelaufen ist. Sitzen deine Leser in Europa, gehört Nordamerika nicht dazu. Mit EmDash gemessen habe ich beides nicht.

Reicht dir beides nicht, bleibt nur der Umzug.

Die Datenbank umziehen

Ein Umzug läuft in drei Etappen: vorbereiten, Daten kopieren, prüfen und umstellen. Die meiste Arbeit steckt im Kopieren.

Vorbereiten

Zuerst kommt ein Schreibstopp. Niemand speichert im Adminbereich, und kein Beitrag ist geplant, denn was nach dem Export geschrieben wird, fehlt sonst in der neuen Datenbank. Außerdem blockiert ein laufender Export laut Doku andere Anfragen.

Dann legst du die neue Datenbank an, mit Standort, aber diesmal ohne --update-config: pnpm wrangler d1 create mein-blog-weur --location weur. Bindet ein deployter Worker sie nämlich schon vor dem Import, migriert und seedet EmDash sie, und der Import kollidiert.

Daten kopieren

Der einfache Export der ganzen Datenbank scheitert bei fast jeder EmDash-Site. Sobald eine Collection durchsuchbar ist, gibt es FTS5-Tabellen, und D1 exportiert keine Datenbanken mit virtuellen Tabellen. Im Blog-Template sind Posts und Pages durchsuchbar. Ob es dich betrifft, verrät diese Abfrage:

pnpm wrangler d1 execute mein-blog --remote --command "SELECT name FROM sqlite_master WHERE sql LIKE '%VIRTUAL TABLE%'"

Kommt nichts zurück, klappt der Export am Stück mit d1 export mein-blog --remote --output dump.sql. Beim Import mit d1 execute mein-blog-weur --remote --file dump.sql wartet aber eine Falle. Seit EmDash 0.37 verweist die Tabelle media auf media_folders, im Dump steht media aber zuerst. Sobald deine Mediathek eine Datei enthält, bricht der Import mit no such table: main.media_folders ab. Verschieb dann die Zeile CREATE TABLE IF NOT EXISTS "media_folders" direkt hinter das PRAGMA in der ersten Zeile, oder nimm gleich den Weg unten.

Hast du Suchtabellen, exportierst du Tabelle für Tabelle und lässt sie weg. Die sind vollständig aus den Inhalten abgeleitet, EmDash baut den Index bei der ersten Suche neu auf.

Dabei lauert eine Falle. Ein Export mit --table enthält Tabellen und Zeilen, aber keine Indizes und keine Trigger. Wer nur Zeilen zählt, merkt davon nichts. Bei uns lief eine umgezogene Datenbank wochenlang mit einem Viertel ihrer Indizes, und jedes Upsert gegen einen fehlenden UNIQUE-Index scheiterte still. So holst du alles, was du brauchst:

# 1. Tabellen ohne Suchindex und Systemtabellen
pnpm wrangler d1 execute mein-blog --remote --json --command "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite%' AND substr(name, 1, 4) <> '_cf_' AND instr(name, '_fts_') = 0 ORDER BY rowid"

# 2. Schema und Zeilen getrennt, mit denselben --table-Flags
pnpm wrangler d1 export mein-blog --remote --no-data --table posts --table … --output schema.sql
pnpm wrangler d1 export mein-blog --remote --no-schema --table posts --table … --output rows.sql

# 3. Indizes und Trigger ohne die der Suche, als dritte Datei
pnpm wrangler d1 execute mein-blog --remote --json --command "SELECT sql FROM sqlite_master WHERE type IN ('index', 'trigger') AND sql IS NOT NULL AND instr(sql, '_fts_') = 0"

Importiert wird in genau dieser Reihenfolge, jeweils mit d1 execute mein-blog-weur --remote --file: erst das Schema, dann die Zeilen, zuletzt Indizes und Trigger. Mit dem Schema zuerst umgehst du das Problem mit media_folders von oben. Und weil die Trigger zuletzt kommen, feuert beim Import keiner.

Bei gut 70 Tabellen im Blog-Template ist das Fleißarbeit.

Prüfen und umstellen

Bevor du umstellst, vergleichst du alt und neu, also die Zeilen pro Tabelle und die Zahl der Indizes und Trigger. Letztere zählst du auf beiden Seiten so:

SELECT type, count(*) FROM sqlite_master WHERE type IN ('index', 'trigger') AND sql IS NOT NULL AND instr(sql, '_fts_') = 0 GROUP BY type

Stimmen die Zahlen, bleiben drei Schritte:

  1. Read Replication auf der neuen Datenbank einschalten, falls du sie nutzt. Die Einstellung hängt an der Datenbank und zieht nicht mit um.
  2. Umstellen. Ändere database_name und database_id in wrangler.jsonc, deploy und prüf dann die Startseite, einen Beitrag und die Anmeldung im Adminbereich.
  3. Die alte Datenbank eine Woche stehen lassen. Sie ist dein Rückweg, und Time Travel fängt auf der neuen Datenbank bei null an.

Ganz ohne SQL geht es über den Site-Transfer. Du exportierst die Website als Paket und bindest die neue Datenbank. Im Einrichtungsassistenten wählst du dann den Import einer bestehenden Site, legst den ersten Admin an und lädst das Paket hoch. Im Paket fehlen aber Benutzer, Passkeys, API-Tokens und alle Plugin-Daten samt Einstellungen, und bis du den Admin angelegt hast, steht der Assistent jedem offen. Für eine Ein-Personen-Site kann das trotzdem der kürzere Weg sein. Für einen Umzug durchgespielt habe ich ihn noch nicht.

Und der Bucket?

Bleibt der Media-Bucket, zum Glück der einfachere Teil. Auch bei R2 steht der Standort mit dem Anlegen fest, aber die Schlüssel ändern sich beim Umzug nicht. Du kopierst die Objekte in einen neuen Bucket, stellst bucket_name um und lässt den alten als Rückweg stehen. Hochladen sollte in der Zeit niemand. Eilig ist es auch weniger, denn ein Bild kostet einen Weg zum Bucket, nicht ein Dutzend, und liegt danach meist im Cache. Im August dauerte bei uns der Abruf einer Datei von München aus mit dem Bucket in WNAM rund 0,23 Sekunden, nach dem Umzug nach WEUR 0,09 bis 0,12 Sekunden.

pnpm wrangler r2 bucket create mein-blog-media-weur --location weur
pnpm wrangler d1 execute mein-blog --remote --json --command "SELECT storage_key, mime_type, size FROM media"
# pro Objekt:
pnpm wrangler r2 object get mein-blog-media/<key> --file tmp/<key> --remote
pnpm wrangler r2 object put mein-blog-media-weur/<key> --file tmp/<key> --content-type <mime> --remote

Stolpern kannst du an drei Stellen:

  • Ein neuer Name. Laut Doku zählt ein Location Hint nur, wenn ein Bucketname zum ersten Mal angelegt wird. Löschst du den Bucket und legst ihn unter demselben Namen neu an, bekommt er den alten Standort zurück.
  • Kein Auflisten. Wrangler kann Objekte nicht auflisten, die Schlüssel kommen deshalb aus der Tabelle media. Vergleich ihre Zahl mit object_count aus r2 bucket info, sonst übersiehst du Objekte, die nicht in media stehen, etwa die Bundles von Plugins aus der Registry.
  • --remote nicht vergessen. Ohne das Flag arbeitet wrangler r2 object lokal, und get legt bei einem fehlenden Schlüssel trotzdem eine leere Datei an. Prüf deshalb die Dateigröße gegen size, nicht den Exit-Code.

Fazit

Kevins Anleitung bringt dich live, und genau dafür ist sie da. Den Standort von D1 und R2 solltest du aber nicht Wrangler überlassen. Mit zwei Befehlen vor dem ersten Deploy legst du ihn selbst fest, und danach kostet er nichts mehr. Vergisst du es, entscheidet Cloudflare nach dem Ort der ersten Anfrage. Uns hat eine Datenbank in Nordamerika nach einem Kaltstart eine bis zwei Sekunden pro ungecachter Seite gekostet. Read Replication hilft beim Lesen, aber deine Redaktion schreibt weiter über den Atlantik. Reparieren kann es nur ein Umzug, und der ist bei einer EmDash-Datenbank mit Suche mehr Arbeit als ein Export. Unsere Site theweeklydash.com läuft mit D1 und R2 in WEUR, die Datenbank mit eingeschalteter Read Replication.

Geprüft mit Wrangler 4.147.0 und dem Blog-Template von EmDash 1.1.0. Die Messwerte stammen von August 2026 auf EmDash 0.3x.

Über den Autor

Daniel Müller

Daniel MüllerEmDash-Maintainer

Daniel Müller baut Websites, die ohne ihn auskommen. Klingt nach schlechtem Geschäft, ist aber sein bestes Argument. Und falls doch mal was ist, geht er selbst ans Telefon, ohne Warteschleife und ohne Ticketsystem. Eisbachcode aus München, Maintainer bei EmDash.

Kommentare

No comments yet

Erstkommentare erscheinen nach unserer Freigabe. Was mit deinen Angaben passiert