Our agent sessions run on Cloudflare Durable Objects (DO below). While debugging a session that kept getting reset by the platform (the full story is in Debugging an agent session that kept running out of memory), we realized our understanding of the DO memory limit was wrong, so we deployed a throwaway probe to measure it. This post only records what we measured and how.
A note on scope: this is a single measurement on our own account on October 26, 2026, not official documentation. The platform's implementation can change at any time, so treat the numbers as a reference point and re-measure in your own environment.
Why we measured
The documented limit is 128 MB, but what we saw in production did not fit: some sessions recorded memory readings of 141 to 167 MB, yet crossing 128 MB did not reliably get them reset. With a strict 128 MB limit that makes no sense. We needed to know what actually gets an object killed before we could set an optimization target.
How we measured
We deployed a temporary Worker containing a single DO class that accepted a few kinds of requests:
- Make garbage: repeatedly allocate and drop large buffers within one request, so total allocation is large but very little is alive at any moment.
- Hold data: allocate a block of random binary data or unique strings and attach it to a field on the DO instance so it stays alive.
- Accumulate: send several requests to the same object, each holding one more block, so live data builds up step by step.
After each request we read the object's memory and error count from Cloudflare's GraphQL analytics API:
durableObjectsPeriodicGroups(filter: { namespaceId: $ns, datetimeFiveMinutes_geq: $from }) {
dimensions { datetimeFiveMinutes objectId }
max { memoryUsageBytes }
sum { exceededMemoryErrors }
}
We also watched the Worker's logs for the platform's reset message. When we were done, we deleted the Worker.
What we measured
| Scenario | Result |
|---|---|
Allocate 2000 MiB of garbage in one request with only about 4 MiB alive at once (tested both with await in between and fully synchronous) |
Survived |
| Hold 110 MiB and 140 MiB of random binary data | Survived |
| Hold about 100 MiB and 200 MiB of unique strings | Survived |
| Accumulate 60 → 120 → 180 → 240 MiB on the same object over several requests | Survived; GraphQL read 254 MiB, 0 memory errors |
| Allocate and hold 300 MiB within one request | Request returned 1101; log: Durable Object's isolate exceeded its memory limit and was reset. |
What we took from it
The real reset line is not 128 MB. The process was still alive at a reading of 254 MiB and was reset when holding 300 MiB. So, at least in this run, the line sat between 254 and 300 MiB. We did not narrow it further; that range is all we know.
What decides life or death is how much data is alive at once, not how fast you allocate. Two gigabytes of garbage in one request was fine, as long as it could be collected. This changed how we looked for the problem: instead of asking what allocates a lot, we looked for which copies are alive at the same moment. Our sessions crossed the line because one large tool output existed in four or five copies at once while it was being processed.
The recorded peak is a lower bound. GraphQL never samples the allocation that triggers the reset, because the process is gone before the next sample. What gets recorded is the last sample before death. Inferring the limit from production peaks will underestimate it.
A reading may not belong to that object alone. On our test environment, seven different DOs reported exactly the same memory peak in the same five-minute bucket. Several DOs can run in the same runtime, and the reading reflects the whole runtime. Read single-object numbers with care, and expect that one object using too much can take its neighbors down too.
The platform logs an explicit cause. On reset, the platform logs Durable Object's isolate exceeded its memory limit and was reset. It does not appear in the log lines your own code writes, but it is in the Workers logs. Alerting on that line is faster than waiting for GraphQL's five-minute buckets, and it points straight at the object.
How we use this now
- Design with headroom. We estimate each session's peak in terms of data alive at once, aim well below the 254 MiB we measured, and no longer rely on the 128 MB figure. Other sessions may share the same runtime, so the headroom is for them too.
- Large inbound content stays out of the DO. Tool output, files and anything else of unbounded size get a byte cap; whatever exceeds it stays outside, and the DO only holds a reference or the head and tail.
- Two kinds of monitoring. GraphQL's
exceededMemoryErrorsfor trends and counts; the platform log line for immediate alerts.
If you run memory-heavy workloads on Durable Objects, it is worth measuring once yourself. The probe is simple, takes half an hour to write and can be deleted afterwards, and it is far more reliable than estimating from the documented number.