Durable-Objects-overloaded-Fehler vollständig verstehen: Das Soft-Limit von 1.000 Requests pro Sekunde und Single-Thread-Engpässe vermeiden

Durable Objects (DO) machen ein verlockendes Versprechen: “Eine global eindeutige Instanz garantiert starke Konsistenz.” Für Domänen, in denen Reihenfolge zählt – Chaträume, Game-Sessions, Zahlungs-State-Machines – ist das ein perfektes Modell. Das Problem entsteht, wenn man beim Design vergisst, warum dieses Versprechen überhaupt funktioniert: eine einzelne DO-Instanz läuft physisch auf genau einem Thread. Steigt der Traffic, kippt das plötzlich in eine Flut von 502ern mit overloaded-Fehlern, eine einzelne langsame externe API verzögert plötzlich alle anderen Nutzer im selben Raum, und Retry-Logik in alarm() führt zu einem Bug, der eine Zahlung zweimal abbucht.
Dieser Beitrag geht diese drei Fallen anhand offizieller Dokumentationswerte und reproduzierbarem Code durch.
Das Wichtigste in Kürze
- Eine einzelne DO-Instanz hat ein Soft-Limit von 1.000 Requests pro Sekunde, und wenn sich Requests für dasselbe Objekt innerhalb eines 10-Sekunden-Fensters stauen, tritt der Fehler
Too many requests for the same object within a 10 second windowauf.await fetch()öffnet das Input Gate und erlaubt Interleaving mit anderen Requests, währendawait storage.*-Operationen undblockConcurrencyWhile()vollständig serialisiert werden und damit echtes Head-of-Line-Blocking erzeugen.- alarm() wird nur automatisch mit exponentiellem Backoff ab 2 Sekunden bis zu maximal 6 Mal wiederholt. Fehlt Side Effects (Zahlungen, Webhooks), die während eines Retries ausgeführt werden, die Idempotenz, kommt es zu Doppelausführungen.
- SQLite-basierte DOs haben ein Speicherlimit von 1 GB (Free) / 10 GB (Paid) pro Objekt; wird es überschritten, schlagen Schreibvorgänge mit
SQLITE_FULLfehl. Seit dem 20. Juli 2026 gibt es im Dashboard ein Namespace-weites “Total storage”-Chart.
Warum DO Single-Threaded ist – und welche Verzögerungen sequenzielle Verarbeitung erzeugt
Der zentrale Vertrag eines Durable Object lautet: “Ein mit derselben ID erzeugtes Objekt läuft weltweit an genau einem physischen Ort, und alle Requests dorthin werden von dieser Instanz in Reihenfolge verarbeitet.” Cloudflares offizielle Dokumentation erklärt das anhand eines Buchungssystems.
“all booking requests for a venue must be serialized to prevent double-booking”
Genau diese Serialisierung ist der Grund, warum sich Doppelbuchungen ohne Locks oder verteilte Transaktionen verhindern lassen. Das ist aber nicht umsonst zu haben: Jede synchrone Ausführung, die für dasselbe Objekt bestimmt ist, muss sich auf der Timeline eines einzigen JS-Threads anstellen. Schauen wir uns einen denkbar einfachen Counter-DO an.
import { DurableObject } from "cloudflare:workers";
export class Counter extends DurableObject {
async fetch(req) {
let count = (await this.ctx.storage.get("count")) ?? 0;
count += 1;
await this.ctx.storage.put("count", count);
return Response.json({ count });
}
}
Dieser Code wirkt für sich genommen harmlos – aber sobald für dieselbe id Hunderte Requests pro Sekunde eingehen, sieht die Sache anders aus. Cloudflares Praxisleitfaden legt den Durchsatz eines einzelnen Objekts konkret fest:
Ein einzelnes Durable Object kann je nach Rechenkomplexität etwa 500 bis 1.000 Requests pro Sekunde verarbeiten.
Die Aussage “DOs skalieren unbegrenzt” ist also nur zur Hälfte richtig. Die Anzahl der Objekte lässt sich beliebig erhöhen, aber der Durchsatz eines einzelnen Objekts kann die Grenze eines einzelnen CPU-Kerns nicht überschreiten. Ein Design mit einem DO pro Chatraum funktioniert innerhalb dieser Grenze hervorragend – sobald man aber sämtlichen Traffic in einem einzigen Objekt bündelt, etwa als “ein globaler Counter” oder “ein globaler Rate-Limiter”, entsteht sofort ein Flaschenhals.
Der ‘Overloaded’-Fehler: Genaue Auslösebedingungen und wie man ihn reproduziert
Das offizielle Soft-Limit und vier Fehlermeldungen
Cloudflares offizielle Limits-Dokumentation legt fest:
“An individual Object has a soft limit of 1,000 requests per second.” “A Durable Object that receives too many requests will, after attempting to queue them, return an overloaded error to the caller.”
Der Begriff “Soft-Limit” ist entscheidend. Es handelt sich nicht um ein Hard-Limit, das sofort beim Überschreiten der 1.000 req/s blockiert, sondern um einen Mechanismus, der ab diesem Punkt versucht, Requests in einer Queue zu puffern, und erst einen Fehler wirft, wenn diese Queue selbst nicht mehr mithält. In den Troubleshooting-Dokumenten wird der overloaded-Fehler tatsächlich nach Ursache in vier verschiedene Meldungen unterteilt:
Durable Object is overloaded. Too many requests queued.— wenn die Anzahl der in der Queue wartenden Requests selbst zu groß istDurable Object is overloaded. Too much data queued.— wenn die Gesamtdatenmenge der in der Queue wartenden Requests zu groß istDurable Object is overloaded. Requests queued for too long.— wenn die Wartezeit des am längsten wartenden Requests in der Queue überschritten istDurable Object is overloaded. Too many requests for the same object within a 10 second window.— das schärfste Überlast-Signal, das nur auftritt, wenn sich für dasselbe Objekt innerhalb eines 10-Sekunden-Fensters eine extrem hohe Anzahl an Requests staut
Die letzte Meldung ist genau die “10-Sekunden-Fenster”-Bedingung aus dem Titel dieses Beitrags. Die Dokumentation erklärt, dass diese Meldung “die anderen overloaded-Meldungen nicht ersetzt, sondern nur bei noch extremerer Überlast zurückgegeben wird”. Wer diese Meldung sieht, hat also bereits eine ziemlich ernste Traffic-Konzentration erlebt.
Ein weiterer leicht übersehener Auslöser ist nicht die Anzahl der Requests selbst, sondern I/O-Latenz. Die Fehlerbehandlungs-Dokumentation schreibt dazu:
Die Ursache einer Infrastruktur-Exception liegt nicht nur in “zu vielen Requests, die sich auf ein einzelnes Durable Object konzentrieren”, sondern auch darin, dass “sich Requests aufgrund langsamer oder übermäßiger I/O in der Queue stauen”.
Das heißt: Selbst wenn die Anzahl der Requests pro Sekunde weit unter 1.000 liegt, kann sich die Queue füllen und overloaded auftreten, wenn jeder einzelne Request lange auf eine langsame externe API wartet.
Reproduktion
Produktions-Traffic künstlich zu belasten ist riskant, daher hier Code, der sich in einer Staging-Umgebung reproduzieren lässt. Der Kern besteht darin, in kurzer Zeit Requests auf dieselbe DO-id zu häufen.
// worker.js — Endpunkt zur Lasterzeugung (nur für Staging)
export default {
async fetch(req, env) {
const url = new URL(req.url);
if (url.pathname === "/hammer") {
const id = env.COUNTER.idFromName("shared-hot-object");
const stub = env.COUNTER.get(id);
const batch = Array.from({ length: 500 }, () =>
stub.fetch("https://do/increment").catch((e) => e)
);
const results = await Promise.allSettled(batch);
const overloaded = results.filter(
(r) => r.status === "fulfilled" && r.value?.status === 500
);
return Response.json({ sent: batch.length, overloaded: overloaded.length });
}
return new Response("ok");
},
};
Hier ist Vorsicht geboten: Das Subrequest-Limit von Workers liegt bei 50 pro Request auf dem Free-Plan und 10.000 pro Request auf dem Paid-Plan, und die Anzahl gleichzeitig offener ausgehender Verbindungen, während auf Response-Header gewartet wird, ist unabhängig vom Plan auf 6 begrenzt. Auch wenn Promise.all so aussieht, als würde es Tausende Requests auf einmal abfeuern, werden sie in Wirklichkeit in Sechserblöcken sequenziell abgearbeitet, und auf dem Free-Plan lassen sich pro Request ohnehin nicht mehr als 50 versenden. Wer wirklich 1.000 req/s erzeugen will, fährt deutlich realistischer damit, den /hammer-Endpunkt mit einem externen Lasttool wie autocannon oder hey mit vielen gleichzeitigen Worker-Aufrufen zu bombardieren.
# Von außen /hammer 10 Sekunden lang mit Concurrency 100 aufrufen
npx autocannon -c 100 -d 10 https://your-worker.workers.dev/hammer
So sendet jeder Worker-Aufruf 500 fetch-Requests an dasselbe DO, und durch die überlappenden gleichzeitigen Aufrufe lässt sich tatsächlich der Fehler Too many requests for the same object within a 10 second window beobachten. Das Error-Objekt trägt eine .overloaded-Eigenschaft, anhand derer man clientseitig unterscheiden sollte:
try {
const resp = await stub.fetch(req);
return resp;
} catch (e: any) {
if (e.overloaded) {
// Ein Retry würde die Überlast nur verschärfen — kein Retry, sofort fehlschlagen lassen
return new Response("busy", { status: 503 });
}
if (e.retryable) {
// Bei idempotenten Requests ist ein Retry mit exponentiellem Backoff möglich
}
throw e;
}
Auch die offizielle Dokumentation warnt klar davor: Wird ein Fehler mit .overloaded === true wiederholt, “verschlimmert das die Überlast und erhöht die Gesamt-Fehlerrate”.
Head-of-Line-Blocking durch langsame externe API-Aufrufe
Was Input Gate und Output Gate tatsächlich tun
Hier entstehen die meisten Missverständnisse. Man nimmt leicht an: “DO ist Single-Threaded, also blockiert ein await fetch() auf eine externe API alle anderen Requests, die an dasselbe Objekt gehen” – das stimmt so nicht. Cloudflare differenziert das über einen Mechanismus namens Input Gate / Output Gate:
- Das Input Gate blockiert neue Events (eingehende Requests, fetch-Antworten) nur, solange synchrone JavaScript-Ausführung läuft.
- Während auf
await fetch()oder KV-Storage-Methoden gewartet wird, bleibt das Input Gate offen, sodass andere Requests dazwischenfahren und ausgeführt werden können. - Beim Warten auf Storage-Operationen (
storage.get/put/transactionusw.) hingegen bleibt das Input Gate geschlossen, sodass keine anderen Requests dazwischenkommen können. - Das Output Gate hält ausgehende Antworten und fetch-Requests zurück, bis ausstehende Storage-Writes abgeschlossen sind — eine “Message sent”-Antwort geht erst hinaus, nachdem die Daten sicher gespeichert wurden.
Ein einzelnes await auf eine externe API blockiert also nicht automatisch das ganze Objekt. Echtes Head-of-Line-Blocking, das tatsächlich andere Requests mitreißt, entsteht bei den folgenden drei Mustern.
Die drei wirklich gefährlichen Muster
1) Ein langsamer externer Aufruf innerhalb von blockConcurrencyWhile(). Diese Methode blockiert bis zum Ende des Callbacks alle Events außer denen, die der Callback selbst erzeugt, in einer Queue. Ein häufiger Fehler ist, im Konstruktor “vorab den Cache aufzuwärmen” und dabei eine externe API zu rufen.
export class RoomState extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
this.ctx.blockConcurrencyWhile(async () => {
// Anti-Pattern: Auf eine langsame externe API im Init-Block warten
this.config = await fetch("https://config-service.example.com/room").then((r) => r.json());
});
}
}
Wird dieser Config-Service langsam oder fällt kurz aus, warten alle Requests an dieses Objekt, bis der Callback abgeschlossen ist. Die offizielle Dokumentation legt fest, dass blockConcurrencyWhile ein Timeout von 30 Sekunden hat und das Durable Object bei Überschreitung “zurückgesetzt wird”. Wirft der Callback eine Exception, wird das Objekt genauso beendet und zurückgesetzt. Die Empfehlung ist eindeutig: “Der Callback sollte so wenig wie möglich tun, damit der gesamte Request-Durchsatz gut bleibt.” Da Operationen auf SQLite-basiertem Storage atomar sind, ist blockConcurrencyWhile für normale Request-Verarbeitung so gut wie nie nötig — es empfiehlt sich, den Einsatz im Wesentlichen auf Schema-Migrationen im Konstruktor zu beschränken.
2) Ein Stau von Storage-Operationen. Anders als await fetch() öffnet ein Aufruf wie await this.ctx.storage.get(...) das Input Gate nicht. Liegt die Ursache also in Storage-I/O, stellen sich Requests tatsächlich in eine Schlange. Die praktische Abhilfe hier: mehrere Keys nicht einzeln mit get() abrufen, sondern in einem Batch bündeln.
// Langsam: N einzelne Roundtrips, während dieser Zeit bleibt das Input Gate durchgehend geschlossen
for (const key of keys) {
await this.ctx.storage.get(key);
}
// Schnell: ein einziger Roundtrip
const values = await this.ctx.storage.get(keys); // keys: Array<string>
3) Synchrone CPU-Operationen. Beim Parsen großer JSON-Payloads oder beim Durchlaufen schwerer Schleifen gibt es von vornherein keinen await-Punkt, weshalb unabhängig vom Input Gate schlicht die Event-Loop selbst blockiert wird. Logik, die große Payloads direkt innerhalb des DO verarbeitet, sollte man entweder in den Worker auslagern oder in Chunks aufteilen.
Lösungsmuster: Requests durch Sharding auf mehrere DO-Instanzen verteilen
Die Erkenntnisse der beiden vorangegangenen Abschnitte laufen auf denselben Punkt hinaus — die Kapazität eines einzelnen Objekts lässt sich nicht erhöhen, also muss die Anzahl der Objekte erhöht werden. Der offizielle Leitfaden nennt “ein einziges globales Durable Object, das alle Chaträume verarbeitet” explizit als Anti-Pattern und formalisiert die benötigte Objektzahl so:
Benötigte Anzahl DOs = (Gesamtzahl Requests pro Sekunde) / (von einem DO verarbeitbare Requests pro Sekunde)
Erhält ein Game-Session-Dienst etwa 500.000 Requests pro Sekunde und ein einzelnes Objekt kann 500 bis 1.000 pro Sekunde verarbeiten, braucht man nicht einen globalen Koordinator, sondern 500 bis 1.000 sessionbezogene DOs. Aus demselben Grund ist es riskant, einen globalen Rate-Limiter als ein einziges DO zu implementieren — er wird zum “Chokepoint”, auf den sich der gesamte Traffic konzentriert, und skaliert dadurch nicht.
In der Praxis shardet man nach natürlichen Partitionsschlüsseln wie User-ID, Room-ID oder Tenant-ID.
// Anti-Pattern: globales Singleton
const id = env.RATE_LIMITER.idFromName("global");
// Besser: pro Nutzer shardieren — jeder Nutzer bekommt ein eigenes Objekt
const id = env.RATE_LIMITER.idFromName(`user:${userId}`);
// Feiner granular: per Hash auf N Buckets verteilen (für näherungsweise globale Aggregation)
function shardId(key, shardCount = 64) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = (hash * 31 + key.charCodeAt(i)) >>> 0;
}
return `shard:${hash % shardCount}`;
}
const id = env.COUNTER.idFromName(shardId(userId));
Natürlich gibt es einen Trade-off. Sobald geshardet wird, lässt sich Logik, die eine “exakte globale Summe” braucht (etwa die Gesamtzahl gleichzeitiger Verbindungen), nicht mehr sofort aus einem einzigen Objekt auslesen. Üblich ist hier ein Fan-in-Muster: Jeder Shard meldet seinen eigenen Zähler periodisch per alarm() an ein übergeordnetes Aggregations-DO — man akzeptiert dabei einen Näherungswert im Sekundenbereich statt Echtzeit-Genauigkeit.
alarm()s exponentieller Backoff-Retry und Doppelausführungs-Bugs durch nicht-idempotenten Code
Warum “maximal 6 Retries” eine Falle ist
alarm() ist die API, die innerhalb eines DO zuverlässige zeitgesteuerte Ausführung bereitstellt. Die offizielle Dokumentation formuliert die Bedingung für diese Zuverlässigkeit so:
“The alarm() handler has guaranteed at-least-once execution and will be retried upon failure using exponential backoff, starting at 2 second delays for up to 6 retries.”
Es wird also nur in Intervallen von 2 → 4 → 8 → 16 → 32 → 64 Sekunden maximal 6 Mal automatisch wiederholt. Und es gibt noch einen wirklich wichtigen zweiten Satz:
“If an unexpected error terminates the Durable Object, the alarm() handler may be re-instantiated on another machine. Following a short delay, the alarm() handler will run from the beginning on the other machine.”
Dieser Satz ist der Kern des Doppelausführungs-Bugs. alarm() wird nicht nur wiederholt, wenn eine Exception geworfen wird — stirbt der DO-Prozess selbst (etwa durch Infrastrukturausfälle, Ressourcenüberschreitung oder unerwarteten Abbruch), wird er von vorne neu ausgeführt. Das heißt, ein Szenario, in dem mitten im alarm()-Handler “der Zahlungsaufruf erfolgreich war, der Prozess aber genau vor dem darauffolgenden Speichern des Status stirbt”, ist real möglich — und der neu gestartete alarm()-Lauf führt dann den bereits erfolgreichen Zahlungsaufruf erneut aus.
// Gefährliches Muster — nicht idempotent
async alarm() {
const order = await this.ctx.storage.get("pendingOrder");
if (!order) return;
// ① Wenn der Prozess direkt nach diesem erfolgreichen fetch, aber vor ② stirbt?
await fetch("https://payments.example.com/charge", {
method: "POST",
body: JSON.stringify(order),
});
// ② Wird das hier nicht erreicht, passiert ① beim nächsten Lauf erneut
await this.ctx.storage.delete("pendingOrder");
}
Die Dokumentation stellt klar, dass ein Aufruf von deleteAlarm() innerhalb des alarm()-Handlers Retries “best-effort verhindern kann, aber nicht garantiert”. deleteAlarm() lässt sich also nicht als Idempotenz-Maßnahme einsetzen.
Muster, die Idempotenz garantieren
Am praktikabelsten ist die Kombination aus den externen Aufruf selbst idempotent machen und den Zustand vor dem Aufruf committen.
async alarm(alarmInfo) {
const order = await this.ctx.storage.get("pendingOrder");
if (!order || order.status === "charged") return;
if (alarmInfo?.isRetry) {
console.log(`retry #${alarmInfo.retryCount}, resuming order ${order.id}`);
}
// 1. Vor dem externen Aufruf zuerst den Status "in Bearbeitung" in den Storage committen
// (order.idempotencyKey wird nur einmal bei der Erstellung vergeben)
await fetch("https://payments.example.com/charge", {
method: "POST",
headers: { "Idempotency-Key": order.idempotencyKey },
body: JSON.stringify(order),
});
// 2. Erst nach Erfolg in den abgeschlossenen Status wechseln
order.status = "charged";
await this.ctx.storage.put("pendingOrder", order);
await this.ctx.storage.delete("pendingOrder"); // Aufräumen
}
Drei Dinge sind hier entscheidend:
- Einen Idempotency-Key an die externe API übergeben. Die meisten Zahlungs-APIs, etwa Stripe, unterstützen den
Idempotency-Key-Header, sodass derselbe Key bei zweifachem Aufruf trotzdem nur einmal tatsächlich abgerechnet wird. - Mit
alarmInfo.isRetry/retryCountloggen, ob es sich um einen Retry handelt, damit man bei später vermuteten Doppelausführungen nachvollziehen kann, was passiert ist. - Wer Exceptions selbst abfängt und eigenständig erneut
setAlarm()setzt (unbegrenzte Retries), sollte bedenken, dass das die Bedeutung eines soliden Idempotenz-Designs nur noch größer macht. Der Wunsch, “öfter als 6 Mal erneut zu versuchen”, ist selbst schon ein Signal, noch einmal zu hinterfragen, ob dieser Side Effect wirklich idempotent ist.
Speicherlimits und Monitoring bei SQLite-basierten DOs
Free/Paid-Limits
Durable Objects verwenden inzwischen standardmäßig SQLite-basierten Storage. Laut offizieller Limits-Dokumentation gelten folgende Grenzen:
- Speicherplatz pro Objekt: 1 GB auf dem Free-Plan, 10 GB auf dem Workers-Paid-Plan
- Speicherplatz pro Account: Auf dem Free-Plan insgesamt 5 GB über den gesamten Account, auf dem Paid-Plan praktisch unbegrenzt (es gilt nur das 10-GB-Limit pro Objekt)
- Key/Value-Größe: Key und Value zusammen dürfen 2 MB nicht überschreiten
- SQL-Beschränkungen: maximal 100 Spalten pro Tabelle, maximal 2 MB pro String/BLOB/Zeile, SQL-Statements maximal 100 KB lang
Was passiert bei Überschreitung des Limits? Die offizielle Dokumentation beschreibt das genau so:
Erreicht ein Objekt sein maximales Speicherlimit (10 GB Paid, 1 GB Free), schlagen Schreiboperationen wie
INSERT,UPDATE,put()odersql.exec()mit dem Fehlerdatabase or disk is full: SQLITE_FULLfehl. Leseoperationen wieSELECT,get(),list()sowieDELETEfunktionieren weiterhin, sodass sich Speicherplatz freigeben lässt.
Eine Limit-Überschreitung führt also nicht zum kompletten Ausfall des Dienstes, sondern nur dazu, dass Schreiben blockiert wird, während sich der Zustand über Löschungen wieder herstellen lässt. Verarbeitungscode sollte entsprechend abgesichert sein:
try {
this.ctx.storage.sql.exec(
"INSERT INTO my_table (key, value) VALUES (?, ?)",
key,
value,
);
} catch (e) {
if (e.message.includes("SQLITE_FULL")) {
// Speicherlimit erreicht — Lesen/Löschen ist weiterhin möglich
// Alte Daten aufräumen oder dem Aufrufer einen aussagekräftigen Fehler zurückgeben
}
throw e;
}
Speicherplatz vorab prüfen
Statt erst nach Erreichen des Limits zu reagieren, ist es deutlich besser, schon vorher per Alarm zu reagieren. Die SQLite-Storage-API stellt eine Property bereit, mit der sich die aktuelle DB-Größe direkt in Bytes auslesen lässt.
async fetch(req) {
const sizeBytes = this.ctx.storage.sql.databaseSize;
const limitBytes = 10 * 1024 * 1024 * 1024; // 10 GB auf dem Paid-Plan
if (sizeBytes > limitBytes * 0.8) {
console.warn(`storage at ${(sizeBytes / limitBytes * 100).toFixed(1)}% of limit`);
// Benachrichtigung senden, Alarm zum Aufräumen alter Zeilen einplanen usw.
}
// ...
}
Wer den gesamten Account im Blick behalten möchte: Cloudflare hat am 20. Juli 2026 dem Durable-Objects-Dashboard ein Namespace-weites “Total storage”-Chart hinzugefügt. Es zeigt den pro Stunde gemeldeten maximalen Speicherverbrauch und eignet sich, um Wachstumstrends zu erkennen, zu prüfen, ob eine Datenbereinigung tatsächlich gewirkt hat, oder unerwartete Nutzungsspitzen zu identifizieren. Das Chart gilt allerdings nur für SQLite-basierte Namespaces und unterstützt noch kein Tracking pro einzelnem Objekt (nach ID/Name) — ob ein bestimmtes Objekt nahe am Limit liegt, muss man also weiterhin wie oben gezeigt direkt im Objekt über databaseSize prüfen.
Fazit: Produktions-Checkliste
Zusammengefasst empfiehlt sich, vor dem Produktiveinsatz von DOs mindestens folgende Punkte zu prüfen:
- Prüfen, ob Traffic-anfällige Keys (globaler Counter, globaler Lock, globaler Rate-Limiter) auf eine einzige DO-id konzentriert werden.
- Prüfen, ob im
blockConcurrencyWhile()-Callback externe API-Aufrufe oder langsame Operationen stecken — das 30-Sekunden-Timeout und den Reset bei Exceptions im Hinterkopf behalten. - Client- bzw. Worker-seitig zwischen
.overloadedund.retryableunterscheiden und bei overloaded keinen Retry auslösen. - Erneut prüfen, ob Side Effects innerhalb von alarm() (Zahlungen, Webhooks, Bestandsabbuchungen) idempotent sind, und wenn möglich den Idempotency-Key der externen API nutzen.
- Bei SQLite-basierten DOs regelmäßig
databaseSizeprüfen oder über das Total-storage-Chart im Dashboard den Trend für den gesamten Account beobachten.
Durable Objects liefern nicht gleichzeitig “starke Konsistenz” und “unbegrenzte Skalierbarkeit”. Die Konsistenz entsteht aus der Single-Thread-Serialisierung eines einzelnen Objekts, die Skalierbarkeit daraus, wie gut man dieses Objekt aufteilt. Ist man sich dieses Trade-offs bereits in der Design-Phase bewusst, lassen sich die oben behandelten Fallen größtenteils von vornherein vermeiden.