Is there an existing issue for this?
How do you use Sentry?
Sentry Saas (sentry.io)
Which SDK are you using?
@sentry/cloudflare
SDK Version
10.53.1
Framework Version
Cloudflare Workers (Hono app, withSentry wrapper), scheduled handler via cron trigger
Link to Sentry event
(none — the failure mode is precisely that no event and no log is produced)
Reproduction Example/SDK Setup
export default Sentry.withSentry(
(env) => ({ dsn: env.SENTRY_DSN, release: env.RELEASE, tracesSampleRate: 0 }),
{
fetch: app.fetch,
scheduled: myScheduledHandler, // ← every tick reports outcome=exception
} satisfies ExportedHandler<Bindings>,
);
Steps to Reproduce
We could not build a minimal repro — wrangler dev and remote preview never exhibit it. It only manifested in the production isolate, with concurrent fetch traffic hitting the same Worker. Filing anyway because the code path is visible by inspection and our elimination evidence is strong; happy to run instrumented builds if it helps.
- Worker wrapped in
withSentry with both fetch and scheduled handlers, cron trigger every minute.
- In production, every single cron tick reports
outcome: exception in the Cloudflare dashboard and in wrangler tail --format json.
- Nothing is thrown from user code: no log lines, no Sentry event, no exception payload in the tail JSON — the handler body demonstrably runs to completion (its side effects are all applied).
Expected Result
A failure inside the SDK's own post-handler flush must not fail the invocation. wrapScheduledHandler should never let its housekeeping reject the waitUntil promise:
// instrumentations/worker/instrumentScheduled.js
} finally {
waitUntil(flushAndDispose(client).catch(() => {
DEBUG_BUILD && debug.warn('Failed to flush/dispose in scheduled handler');
}));
}
(Or make flushAndDispose itself never-reject — every wrapper call site passes it raw: instrumentScheduled.js, instrumentQueue.js, instrumentEmail.js, instrumentTail.js, and the five waitUntil?.(flushAndDispose(client)) sites in request.js.)
Actual Result
In @sentry/cloudflare@10.53.1, wrapScheduledHandler does:
} finally {
waitUntil(flushAndDispose(client));
}
with
async function flushAndDispose(client, timeout = 2000) {
...
await client.flush(timeout);
client.dispose();
}
On Workers, a rejected promise passed to ctx.waitUntil fails the whole invocation — the runtime records outcome: exception even though the handler returned normally. For a fetch handler this is mostly invisible (the response already went out); for a cron it flips every tick to "failed" in the dashboard, silently: the rejection happens after user code, so nothing is loggable from the app and the SDK (whose transport just failed / whose client is mid-dispose) can't report its own demise.
Elimination evidence from our production incident:
- Every cron tick =
outcome: exception, zero logs, zero exceptions in tail JSON, handler side effects all present.
- Three consecutive releases added progressively more defensive guards inside our handler (whole-body try/catch, guarded resource teardown so nothing user-side could reject) — no effect whatsoever.
- Moving
scheduled outside withSentry (keeping our own direct envelope delivery for errors) fixed it instantly: same handler code, outcome: ok on every tick since, across weeks.
We did not manage to capture the underlying rejection value (by nature it never surfaces anywhere we can hook). Plausible candidates are client.flush() rejecting when the isolate's subrequest is canceled at teardown, or dispose() throwing on an already-disposed client under concurrent invocations sharing the isolate — possibly related to the client-lifetime issues in #20533 and #19589, but the failure mode here is different: the invocation itself is marked failed by the raw waitUntil.
Our current workaround is keeping the scheduled handler unwrapped, which means losing faas.cron spans and automatic capture for crons — we'd love to re-wrap once the flush is guarded.
Is there an existing issue for this?
How do you use Sentry?
Sentry Saas (sentry.io)
Which SDK are you using?
@sentry/cloudflare
SDK Version
10.53.1
Framework Version
Cloudflare Workers (Hono app,
withSentrywrapper), scheduled handler via cron triggerLink to Sentry event
(none — the failure mode is precisely that no event and no log is produced)
Reproduction Example/SDK Setup
Steps to Reproduce
We could not build a minimal repro —
wrangler devand remote preview never exhibit it. It only manifested in the production isolate, with concurrent fetch traffic hitting the same Worker. Filing anyway because the code path is visible by inspection and our elimination evidence is strong; happy to run instrumented builds if it helps.withSentrywith bothfetchandscheduledhandlers, cron trigger every minute.outcome: exceptionin the Cloudflare dashboard and inwrangler tail --format json.Expected Result
A failure inside the SDK's own post-handler flush must not fail the invocation.
wrapScheduledHandlershould never let its housekeeping reject thewaitUntilpromise:(Or make
flushAndDisposeitself never-reject — every wrapper call site passes it raw:instrumentScheduled.js,instrumentQueue.js,instrumentEmail.js,instrumentTail.js, and the fivewaitUntil?.(flushAndDispose(client))sites inrequest.js.)Actual Result
In
@sentry/cloudflare@10.53.1,wrapScheduledHandlerdoes:with
On Workers, a rejected promise passed to
ctx.waitUntilfails the whole invocation — the runtime recordsoutcome: exceptioneven though the handler returned normally. For a fetch handler this is mostly invisible (the response already went out); for a cron it flips every tick to "failed" in the dashboard, silently: the rejection happens after user code, so nothing is loggable from the app and the SDK (whose transport just failed / whose client is mid-dispose) can't report its own demise.Elimination evidence from our production incident:
outcome: exception, zero logs, zero exceptions in tail JSON, handler side effects all present.scheduledoutsidewithSentry(keeping our own direct envelope delivery for errors) fixed it instantly: same handler code,outcome: okon every tick since, across weeks.We did not manage to capture the underlying rejection value (by nature it never surfaces anywhere we can hook). Plausible candidates are
client.flush()rejecting when the isolate's subrequest is canceled at teardown, ordispose()throwing on an already-disposed client under concurrent invocations sharing the isolate — possibly related to the client-lifetime issues in #20533 and #19589, but the failure mode here is different: the invocation itself is marked failed by the rawwaitUntil.Our current workaround is keeping the scheduled handler unwrapped, which means losing
faas.cronspans and automatic capture for crons — we'd love to re-wrap once the flush is guarded.