denoland / denoland/celld

Nested waitUntil() rejects during active background work after a Durable Object response returns

Closed
#202 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
4.6k
Forks
171
PR merge metrics
No merged PRs in 30d

Description

**Version:** celld 0.4.1

A Durable Object registers background work with `ctx.waitUntil()` and returns its HTTP response. While that background work is still running, calling the imported `waitUntil()` from `cloudflare:workers` throws:

```text
Disallowed operation called within global scope.
```

This is active request-associated background work, not module/global initialization.

### Reduced reproduction

```js
import { waitUntil } from "cloudflare:workers";

export class Repro {
constructor(ctx) {
this.ctx = ctx;
}

fetch() {
this.ctx.waitUntil(
(async () => {
await new Promise((resolve) => setTimeout(resolve, 10));

// Runs after the response returns, while the outer task is pending.
const pending = fetch("https://example.com");
waitUntil(pending); // Throws the global-scope error.
})(),
);

return new Response("accepted");
}
}
```

### Expected behavior

Additional `waitUntil()` work can extend the lifetime while previously registered background work remains pending.

### Actual behavior

Registration is rejected after the response returns. Once the original background task settles, unfinished native operations are canceled.

### Impact

Our background OpenTelemetry exporter catches the registration exception and returns without awaiting its export. Celld then cancels both the exporter fetch and its timeout. The collector can receive spans followed by `context canceled`, while the application receives neither an export result nor a timeout.

### Verified workaround

Return the export promise from our observer callback, allowing the already-registered outer background task to await it.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the reduced Durable Object reproduction and the `ctx.waitUntil()` and imported `cloudflare:workers` `waitUntil()` entry points described in the issue. Trace why registration is treated as global scope after the response returns, then verify that nested work remains registered while the outer background task is pending and that the cancellation behavior no longer occurs.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, rust
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.