[cloudflare] scheduled handler: raw waitUntil(flushAndDispose(client)) fails every cron invocation with silent outcome=exception #21950

Description

@aabrius

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

exportdefaultSentry.withSentry((env)=>({dsn: env.SENTRY_DSN,release: env.RELEASE,tracesSampleRate: 0}),{fetch: app.fetch,scheduled: myScheduledHandler,// ← every tick reports outcome=exception}satisfiesExportedHandler<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.

  1. Worker wrapped in withSentry with both fetch and scheduled handlers, cron trigger every minute.
  2. In production, every single cron tick reports outcome: exception in the Cloudflare dashboard and in wrangler tail --format json.
  3. 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

asyncfunctionflushAndDispose(client,timeout=2000){
...
awaitclient.flush(timeout);client.dispose();}

On Workers, a rejected promise passed toctx.waitUntilfails 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 scheduledoutsidewithSentry (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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[cloudflare] scheduled handler: raw waitUntil(flushAndDispose(client)) fails every cron invocation with silent outcome=exception #21950

Description

@aabrius

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

exportdefaultSentry.withSentry((env)=>({dsn: env.SENTRY_DSN,release: env.RELEASE,tracesSampleRate: 0}),{fetch: app.fetch,scheduled: myScheduledHandler,// ← every tick reports outcome=exception}satisfiesExportedHandler<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.

  1. Worker wrapped in withSentry with both fetch and scheduled handlers, cron trigger every minute.
  2. In production, every single cron tick reports outcome: exception in the Cloudflare dashboard and in wrangler tail --format json.
  3. 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

asyncfunctionflushAndDispose(client,timeout=2000){
...
awaitclient.flush(timeout);client.dispose();}

On Workers, a rejected promise passed toctx.waitUntilfails 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 scheduledoutsidewithSentry (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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[cloudflare] scheduled handler: raw waitUntil(flushAndDispose(client)) fails every cron invocation with silent outcome=exception #21950

Description

@aabrius

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

exportdefaultSentry.withSentry((env)=>({dsn: env.SENTRY_DSN,release: env.RELEASE,tracesSampleRate: 0}),{fetch: app.fetch,scheduled: myScheduledHandler,// ← every tick reports outcome=exception}satisfiesExportedHandler<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.

  1. Worker wrapped in withSentry with both fetch and scheduled handlers, cron trigger every minute.
  2. In production, every single cron tick reports outcome: exception in the Cloudflare dashboard and in wrangler tail --format json.
  3. 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

asyncfunctionflushAndDispose(client,timeout=2000){
...
awaitclient.flush(timeout);client.dispose();}

On Workers, a rejected promise passed toctx.waitUntilfails 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 scheduledoutsidewithSentry (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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[cloudflare] scheduled handler: raw waitUntil(flushAndDispose(client)) fails every cron invocation with silent outcome=exception #21950

Description

@aabrius

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

exportdefaultSentry.withSentry((env)=>({dsn: env.SENTRY_DSN,release: env.RELEASE,tracesSampleRate: 0}),{fetch: app.fetch,scheduled: myScheduledHandler,// ← every tick reports outcome=exception}satisfiesExportedHandler<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.

  1. Worker wrapped in withSentry with both fetch and scheduled handlers, cron trigger every minute.
  2. In production, every single cron tick reports outcome: exception in the Cloudflare dashboard and in wrangler tail --format json.
  3. 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

asyncfunctionflushAndDispose(client,timeout=2000){
...
awaitclient.flush(timeout);client.dispose();}

On Workers, a rejected promise passed toctx.waitUntilfails 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 scheduledoutsidewithSentry (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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[cloudflare] scheduled handler: raw waitUntil(flushAndDispose(client)) fails every cron invocation with silent outcome=exception #21950

Description

@aabrius

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

exportdefaultSentry.withSentry((env)=>({dsn: env.SENTRY_DSN,release: env.RELEASE,tracesSampleRate: 0}),{fetch: app.fetch,scheduled: myScheduledHandler,// ← every tick reports outcome=exception}satisfiesExportedHandler<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.

  1. Worker wrapped in withSentry with both fetch and scheduled handlers, cron trigger every minute.
  2. In production, every single cron tick reports outcome: exception in the Cloudflare dashboard and in wrangler tail --format json.
  3. 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

asyncfunctionflushAndDispose(client,timeout=2000){
...
awaitclient.flush(timeout);client.dispose();}

On Workers, a rejected promise passed toctx.waitUntilfails 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 scheduledoutsidewithSentry (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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[cloudflare] scheduled handler: raw waitUntil(flushAndDispose(client)) fails every cron invocation with silent outcome=exception #21950

Description

@aabrius

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

exportdefaultSentry.withSentry((env)=>({dsn: env.SENTRY_DSN,release: env.RELEASE,tracesSampleRate: 0}),{fetch: app.fetch,scheduled: myScheduledHandler,// ← every tick reports outcome=exception}satisfiesExportedHandler<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.

  1. Worker wrapped in withSentry with both fetch and scheduled handlers, cron trigger every minute.
  2. In production, every single cron tick reports outcome: exception in the Cloudflare dashboard and in wrangler tail --format json.
  3. 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

asyncfunctionflushAndDispose(client,timeout=2000){
...
awaitclient.flush(timeout);client.dispose();}

On Workers, a rejected promise passed toctx.waitUntilfails 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 scheduledoutsidewithSentry (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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[cloudflare] scheduled handler: raw waitUntil(flushAndDispose(client)) fails every cron invocation with silent outcome=exception #21950

Description

@aabrius

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

exportdefaultSentry.withSentry((env)=>({dsn: env.SENTRY_DSN,release: env.RELEASE,tracesSampleRate: 0}),{fetch: app.fetch,scheduled: myScheduledHandler,// ← every tick reports outcome=exception}satisfiesExportedHandler<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.

  1. Worker wrapped in withSentry with both fetch and scheduled handlers, cron trigger every minute.
  2. In production, every single cron tick reports outcome: exception in the Cloudflare dashboard and in wrangler tail --format json.
  3. 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

asyncfunctionflushAndDispose(client,timeout=2000){
...
awaitclient.flush(timeout);client.dispose();}

On Workers, a rejected promise passed toctx.waitUntilfails 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 scheduledoutsidewithSentry (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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[cloudflare] scheduled handler: raw waitUntil(flushAndDispose(client)) fails every cron invocation with silent outcome=exception #21950

Description

@aabrius

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

exportdefaultSentry.withSentry((env)=>({dsn: env.SENTRY_DSN,release: env.RELEASE,tracesSampleRate: 0}),{fetch: app.fetch,scheduled: myScheduledHandler,// ← every tick reports outcome=exception}satisfiesExportedHandler<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.

  1. Worker wrapped in withSentry with both fetch and scheduled handlers, cron trigger every minute.
  2. In production, every single cron tick reports outcome: exception in the Cloudflare dashboard and in wrangler tail --format json.
  3. 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

asyncfunctionflushAndDispose(client,timeout=2000){
...
awaitclient.flush(timeout);client.dispose();}

On Workers, a rejected promise passed toctx.waitUntilfails 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 scheduledoutsidewithSentry (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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions