Skip to content

computer: Keep long-running isolate JavaScript alive - #237

Merged
aron-cf merged 10 commits into
mainfrom
computer/long-running-js-exec
Oct 15, 2026
Merged

aron-cf merged 10 commits into
mainfrom
computer/long-running-js-exec

Conversation

@scuffi

@scuffi scuffi commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

Isolate JavaScript can't run in the background for long yet. runtime.exec() returns straight away, but the run only lives as long as its Durable Object stays in memory:

  • Before compatibility date 2026-10-01, that's about a minute after the request that started the run ends.
  • From 2026-10-01, pending work keeps the object in memory, but each piece of work only counts for 15 minutes. A run that goes quiet for that long (no host calls, no output) gets evicted and dies.

The backend also used each run's timeout as its CPU limit, so a two-hour timeout asked for two hours of CPU.

Changes

  • Keep-alive timer. While any run is in flight, the backend schedules a timer every keepAliveIntervalMs (60 seconds by default, 0 turns it off, 15 minutes or more is rejected). Each timer counts as new pending work, so the object stays in memory for the whole run. The timer stops when the last run settles. The backend reads the host Worker's compatibility flags and only schedules the timer where pending work can keep the object in memory, so earlier dates get no timer at all. A run also stops counting once its timeout plus maxHostCallMs has passed, so a host module that never settles can't pin the object forever.
  • maxCpuMs. Caps a run's CPU limit below its timeout. It defaults to maxTimeoutMs, so nothing changes unless you set it.
  • Docs. docs/17_isolate_javascript.md gets a "Long-running executions" section, including the per-run limits a long run hits first. It also drops a paragraph claiming that pending work keeps the object resident and that reading the event stream keeps a run alive, neither of which is true.

Earlier compatibility dates behave exactly as before. A deploy or crash still ends a run, and the run is marked as failed the next time the backend connects.

Trying it

Use compatibility date 2026-10-01 or later, and raise the limits that grow with run length:

new WorkerJavaScriptBackend({
  loader: env.LOADER,
  maxTimeoutMs: 2 * 60 * 60_000,
  maxCapabilityCalls: 100_000,
  maxCapabilityResponseBytes: 256 * 1024 * 1024,
});

Then start a long run from a Durable Object method that returns without awaiting it, and check the result with getExec later.

Testing

Unit tests cover the timer (including kill, close, and the compatibility-flag gate) and the CPU limit. In production, Cloudflare.compatibilityFlags.durable_object_io_tasks_prevent_eviction read false on a Worker dated 2026-06-01 and true on one dated 2026-10-12. I also ran this branch in production on compatibility date 2026-10-12, starting every run from a method that returned straight away:

Run Result
2 hours, writing a file and printing every 15 s Completed
25 min, no host calls or output, timer off Evicted at ~15 min
Same, timer on Completed
~100% and ~40% CPU, cpuMs 400000 Both stopped at ~5 min of CPU
~100% CPU, default maxCpuMs with a 2 hour timeout (cpuMs 7200000) Accepted, stopped at ~5 min of CPU
Three runs back to back, ~195 s of CPU each All completed

Caveats

  • A Dynamic Worker call gets about 5 minutes of CPU in total, whatever limit you ask for. Runs that mostly wait on I/O can last hours; heavy compute needs splitting across executions (each gets a fresh budget) or moving to a container.
  • Several limits count over the whole run: maxCapabilityCalls (256), capability request and response bytes (8 MiB each), output (1 MiB), and the platform's subrequest limit.
  • While a run is in flight the object is billed for duration and can't hibernate, and the run holds one of the ten Dynamic Workers an object can have in flight.

Follow-ups

The doc comment on WorkerJavaScriptBackendOptions.retentionMs said
completed executions are kept for five minutes. The backend keeps them
for 60 minutes.
WorkerJavaScriptBackend passed each execution's timeout to the Worker
Loader as its CPU limit, so a run allowed to last two hours also asked
for two hours of CPU. A long run that mostly waits on I/O needs a long
timeout but little CPU.

Add a maxCpuMs option. Each execution's CPU limit is the smaller of its
timeout and maxCpuMs, which defaults to maxTimeoutMs so existing
configurations behave as before.

CPU time adds up over the whole Dynamic Worker call, and Cloudflare
currently stops a call at about five minutes of CPU whatever the limit,
so values above 300000 have no effect. The option comment says so.
From compatibility date 2026-10-01, pending work such as a call into a
Dynamic Worker keeps a Durable Object in memory for at most 15 minutes
from when it starts. An execution is one such call. If it makes no host
calls and prints nothing for that long, for example while it waits on
a slow fetch, the object is evicted and the execution dies with it.
Before that date, pending work does not keep the object in memory at
all.

While any execution is running, the backend now schedules an interval
timer every keepAliveIntervalMs, 60 seconds by default. Each timer
counts as new pending work, so the object stays in memory for as long
as an execution runs. The timer is cleared when the last execution
settles, so an idle Workspace can still be evicted or hibernate. Set
keepAliveIntervalMs to 0 to turn it off, for example when the host
keeps the object in memory itself.
Add a section on running isolate JavaScript for longer than the default
three minutes: which limits to raise, how the compatibility date
decides whether the Durable Object stays in memory, how CPU time is
limited separately from wall-clock time, and that nothing carries a run
across a deploy or a crash.

Replace the paragraph that said pending work keeps the Durable Object
resident and that reading the event stream keeps a run alive. Neither
holds: before compatibility date 2026-10-01 pending work does not keep
the object in memory, and from that date it does so for at most 15
minutes per piece of work.
@changeset-bot

changeset-bot Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

馃 Changeset detected

Latest commit: d2d5328

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@cloudflare/computer Minor
@cloudflare/dofs Minor
@cloudflare/computer-rpc Minor
@cloudflare/computerd Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@cloudflare/computer@237

commit: d2d5328

When an execution times out or is killed, the backend waits for host
calls it already accepted before it records the execution as finished.
A host module that ignores its abort signal and never settles leaves
the execution finalizing forever, and the keep-alive timer kept the
Durable Object in memory, and billed, for as long as that lasted.
Without the timer the object would eventually have been evicted.

Give each running execution a keep-alive deadline of its timeout plus
maxHostCallMs, the longest a well-behaved host call accepted at the
last moment can take. The timer drops executions past their deadline
when it fires and stops once none are left, so the platform's own
eviction rules apply again.
Pending work protects a Durable Object for at most 15 minutes from when
it starts, so a keep-alive timer with a longer interval leaves gaps in
which the object can be evicted mid-execution. The option accepted any
finite value. Refuse 15 minutes or more when the backend is
constructed.
The long-running section only mentioned maxCapabilityCalls. A run also
reaches the cumulative capability request and response byte limits,
the shared standard output and error limit, and the platform's
subrequest limit, all counted over the whole execution. List them with
their defaults, and note that the keep-alive timer stops counting a run
past its timeout plus maxHostCallMs, that the object is billed and
can't hibernate while it runs, and that a long run holds one of the
ten Dynamic Workers a Durable Object can have in flight.
Replace the two changesets for maxCpuMs and the keep-alive timer with
one short entry covering both.
@scuffi
scuffi force-pushed the computer/long-running-js-exec branch from df853a1 to 123b64d Compare October 14, 2026 09:39
@scuffi
scuffi marked this pull request as ready for review October 14, 2026 14:24
devin-ai-integration[bot]

This comment was marked as resolved.

Pending work keeps a Durable Object in memory only when the host
Worker's compatibility date is 2026-10-01 or later, or it sets
durable_object_io_tasks_prevent_eviction. On earlier dates the
keep-alive timer did nothing but fire every minute while a run was in
flight.

The backend now reads the host's flags from Cloudflare.compatibilityFlags
when it connects and schedules the timer only when that flag is on. The
Dynamic Worker's own compatibility date plays no part. Outside the
Workers runtime, where the flags can't be read, the timer is scheduled
as before, which is harmless. A host that moves to a newer date gets the
timer without changing any configuration.
Killing the last running execution and closing the Workspace with
executions running both clear the keep-alive timer. Only a run settling
on its own was tested before.
@aron-cf
aron-cf merged commit 7415a44 into main Oct 15, 2026
24 checks passed
@aron-cf
aron-cf deleted the computer/long-running-js-exec branch October 15, 2026 13:57
@github-actions github-actions Bot mentioned this pull request Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants