You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Plugin Jobs: host-run, resumable background work for sandboxed plugins
#3869
On Cloudflare, a sandboxed plugin invocation can make 10 host calls. Every ctx.storage, ctx.kv and ctx.http call counts, because storage crosses the PluginBridge service binding. That's fine for most requests, but any operation whose work grows with its input can't finish:
importing or migrating 500 products;
updating stock for each line of a 30-line order after payment;
re-indexing a collection or rebuilding a feed;
sending a newsletter to N subscribers;
reconciling N records against a third-party API.
The only background primitive today is cron. Chaining one-shot cron tasks works, but each job moves forward one step per tick (once a minute on Cloudflare), and each tick runs at most 10 tasks, one after another. The plugin also has to spend its own call budget on storing progress, retries have no backoff or failure state, and operators can't see or retry anything.
What we measured
We deployed EmDash 1.1.0 on Workers Paid (D1 + Worker Loader) with a small probe plugin:
Test
Result
Sequential ctx.storage calls
10 succeed; the 11th throws Too many subrequests by single Worker invocation
Sequential ctx.kv calls
Same: 10, then the 11th throws
9 get calls plus one getMany(50)
Succeeds, so a batch counts as one call
Largest getMany
95 ids work; 99 fail with D1_ERROR: too many SQL variables
CPU
~1.3 s of pure CPU passes; ~2.2 s fails. Calls are the real limit, not CPU
Raw route body (body: "bytes") + declared header
Byte-identical. Great for webhook signatures 👍
For a real case: the commerce plugin we're building makes 21–84 storage calls per checkout or payment webhook in its current form. Batching and a leaner storage layout will cut that a lot. But committing stock means one compare-and-set per order line, and there's no batch compare-and-set, so some work stays proportional to the order.
Proposed direction
Let the host run long work as a series of small, resumable steps, and keep the business logic in the plugin:
// enqueue from a route, hook or cron (one host call)constjob=awaitctx.jobs.create("stock-commit",{ orderId },{key: orderId});// the plugin implements one bounded step; the host calls it repeatedly
jobs: {"stock-commit": async({ job, cursor },ctx)=>{// ...do at most ~8 host calls of work starting at `cursor`returndone ? {done: true} : {done: false,cursor: next,progress: { processed, total }};},}
The host owns everything around the steps:
Fresh budget per step: each step is a new sandbox invocation with the full limits.
Free bookkeeping: the cursor, progress and attempt count are stored by the host, not in plugin storage, so they cost the plugin no calls.
Clear delivery rules: a step can run more than once and must be safe to repeat; steps of one job run in order; retries back off and end in a visible failed state.
Platforms: Cloudflare Queues on Cloudflare, falling back to the existing cron scheduler; a database-backed runner on Node. The plugin API is the same everywhere, sandboxed and native.
Admin: a Jobs screen with progress, Retry and Cancel.
This is roughly what WooCommerce's Action Scheduler does for WordPress, adapted to EmDash's sandbox.
A full RFC draft (API types, state machine, host table, Cloudflare and Node execution, security, testing, alternatives) is here: chrmoller#1
Questions for maintainers
Is host-run background work something you'd want in core, or should plugins keep building it on cron?
Would you prefer extending cron (one-shot tasks plus cursor and retry semantics) over a separate jobs concept?
Are you open to sites on Cloudflare adding a Queues binding, with cron as the fallback?
Separately: is making the sandbox limits configurable on the table? It's complementary, since Cloudflare's 32-invocations-per-request cap still bounds a single request.
We're happy to implement the first phase (host table, executor on the existing scheduler, ctx.jobs, test-host support) if the direction makes sense.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
The problem
On Cloudflare, a sandboxed plugin invocation can make 10 host calls. Every
ctx.storage,ctx.kvandctx.httpcall counts, because storage crosses thePluginBridgeservice binding. That's fine for most requests, but any operation whose work grows with its input can't finish:The only background primitive today is cron. Chaining one-shot cron tasks works, but each job moves forward one step per tick (once a minute on Cloudflare), and each tick runs at most 10 tasks, one after another. The plugin also has to spend its own call budget on storing progress, retries have no backoff or failure state, and operators can't see or retry anything.
What we measured
We deployed EmDash 1.1.0 on Workers Paid (D1 + Worker Loader) with a small probe plugin:
ctx.storagecallsToo many subrequests by single Worker invocationctx.kvcallsgetcalls plus onegetMany(50)getManyD1_ERROR: too many SQL variablesbody: "bytes") + declared headerFor a real case: the commerce plugin we're building makes 21–84 storage calls per checkout or payment webhook in its current form. Batching and a leaner storage layout will cut that a lot. But committing stock means one compare-and-set per order line, and there's no batch compare-and-set, so some work stays proportional to the order.
Proposed direction
Let the host run long work as a series of small, resumable steps, and keep the business logic in the plugin:
The host owns everything around the steps:
failedstate.This is roughly what WooCommerce's Action Scheduler does for WordPress, adapted to EmDash's sandbox.
A full RFC draft (API types, state machine, host table, Cloudflare and Node execution, security, testing, alternatives) is here: chrmoller#1
Questions for maintainers
jobsconcept?We're happy to implement the first phase (host table, executor on the existing scheduler,
ctx.jobs, test-host support) if the direction makes sense.All reactions