fix(gmail): bound incremental sync per pass to stop isolate OOM - #241

Merged
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom
Jun 29, 2026
Merged

fix(gmail): bound incremental sync per pass to stop isolate OOM#241
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

The mailbox-wide Gmail incremental sync drained the entire changed-thread set of a history window in a single isolate pass — syncGmailMailboxIncremental collected every changed thread id and getThread()'d all of them into one in-memory array (the comment even said "a single pass drains the whole history window").

For a large window — the history-cursor reseed after the 2026-06-25 Google composite re-home, or a high-volume burst — this loaded hundreds of full threads at once and exceeded the Cloudflare Worker memory limit (Worker exceeded memory limit., PostHog issue 019ed581).

The OOM killed the isolate mid-save: the API worker's request-scoped Kysely connection was torn down in its finally while in-flight saveLink/createLink work was still querying it, surfacing as driver has already been destroyed (PostHog 019f1398 / 019f1399) and dropping mail. The fetch storm also tripped Gmail's per-minute API quota (403s).

Evidence

For the affected user's connector, the per-user error timeline interleaves Worker exceeded memory limit. (bursts of 8–9), Gmail 403 Quota exceeded … Queries per minute per user, and driver has already been destroyed — the driver errors land immediately after each OOM burst.

Fix

Bound each incremental pass to MAX_INCREMENTAL_THREADS_PER_BATCH = 20 (matching the proven-safe initial-backfill page size):

  • syncGmailMailboxIncremental fetches at most the cap and returns the overflow as deferredThreadIds. retryThreadIds are inserted first, so prior backlog drains ahead of newly-changed threads.
  • mergePendingThreads(prior, failed, deferred?) carries deferred ids forward without bumping the failed-fetch attempt counter — they were postponed, not tried — so a large backlog drains instead of being abandoned.
  • incrementalSyncBatchFn + the self-heal path carry deferred ids in pendingThreadIds and schedule a self-terminating continuation via the existing queueIncrementalSync scheduler hook (exit condition: nothing deferred; failed-only does not re-queue, so a permanently-unfetchable thread can't hot-loop).

No connector-class changes — the composite Google connector inherits the fix through the same shared functions.

Tests

  • New gmail-incremental-bound.test.ts (8 tests): per-pass cap + deferral, retry-id ordering, back-compat (unbounded when maxThreads omitted), deferred-carry without attempt bump, failed-vs-deferred distinction, and the incrementalSyncBatchFn cap + cursor-advance + continuation wiring.
  • Full suites pass: gmail (63), google (64). tsc + plot lint clean.

Connector-only change → no changeset.

🤖 Generated with Claude Code

The mailbox-wide incremental sync drained the entire changed-thread set of
a history window in a single isolate pass — `syncGmailMailboxIncremental`
collected every changed thread id and `getThread()`'d all of them into one
in-memory array ("a single pass drains the whole history window"). For a
large window (e.g. the history-cursor reseed after the 2026-06-25 Google
composite re-home, or a high-volume burst) this loaded hundreds of full
threads at once and exceeded the Cloudflare Worker memory limit.
The OOM killed the isolate mid-save: the API worker's request-scoped Kysely
connection was torn down in its `finally` while in-flight saveLink/createLink
work still queried it, surfacing as "driver has already been destroyed"
(PostHog 019f1398 / 019f1399) and dropping mail; the fetch storm also tripped
Gmail's per-minute API quota.
Bound each pass to MAX_INCREMENTAL_THREADS_PER_BATCH (20, matching the
initial-backfill page size). Overflow is returned as `deferredThreadIds`,
carried forward in `pendingThreadIds` WITHOUT bumping the failed-fetch attempt
counter (they were postponed, not tried), and drained on a self-terminating
continuation via the existing `queueIncrementalSync` scheduler hook. The
self-heal path is bounded the same way. retryThreadIds sort first, so prior
backlog drains ahead of newly-changed threads. No connector-class changes —
the composite Google connector inherits the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit d6b466b into mainJun 29, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/gmail-incremental-sync-bound-oom branch June 29, 2026 18:09
Sign up for freeto 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.

1 participant

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 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

fix(gmail): bound incremental sync per pass to stop isolate OOM - #241

Merged
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom
Jun 29, 2026
Merged

fix(gmail): bound incremental sync per pass to stop isolate OOM#241
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

The mailbox-wide Gmail incremental sync drained the entire changed-thread set of a history window in a single isolate pass — syncGmailMailboxIncremental collected every changed thread id and getThread()'d all of them into one in-memory array (the comment even said "a single pass drains the whole history window").

For a large window — the history-cursor reseed after the 2026-06-25 Google composite re-home, or a high-volume burst — this loaded hundreds of full threads at once and exceeded the Cloudflare Worker memory limit (Worker exceeded memory limit., PostHog issue 019ed581).

The OOM killed the isolate mid-save: the API worker's request-scoped Kysely connection was torn down in its finally while in-flight saveLink/createLink work was still querying it, surfacing as driver has already been destroyed (PostHog 019f1398 / 019f1399) and dropping mail. The fetch storm also tripped Gmail's per-minute API quota (403s).

Evidence

For the affected user's connector, the per-user error timeline interleaves Worker exceeded memory limit. (bursts of 8–9), Gmail 403 Quota exceeded … Queries per minute per user, and driver has already been destroyed — the driver errors land immediately after each OOM burst.

Fix

Bound each incremental pass to MAX_INCREMENTAL_THREADS_PER_BATCH = 20 (matching the proven-safe initial-backfill page size):

  • syncGmailMailboxIncremental fetches at most the cap and returns the overflow as deferredThreadIds. retryThreadIds are inserted first, so prior backlog drains ahead of newly-changed threads.
  • mergePendingThreads(prior, failed, deferred?) carries deferred ids forward without bumping the failed-fetch attempt counter — they were postponed, not tried — so a large backlog drains instead of being abandoned.
  • incrementalSyncBatchFn + the self-heal path carry deferred ids in pendingThreadIds and schedule a self-terminating continuation via the existing queueIncrementalSync scheduler hook (exit condition: nothing deferred; failed-only does not re-queue, so a permanently-unfetchable thread can't hot-loop).

No connector-class changes — the composite Google connector inherits the fix through the same shared functions.

Tests

  • New gmail-incremental-bound.test.ts (8 tests): per-pass cap + deferral, retry-id ordering, back-compat (unbounded when maxThreads omitted), deferred-carry without attempt bump, failed-vs-deferred distinction, and the incrementalSyncBatchFn cap + cursor-advance + continuation wiring.
  • Full suites pass: gmail (63), google (64). tsc + plot lint clean.

Connector-only change → no changeset.

🤖 Generated with Claude Code

The mailbox-wide incremental sync drained the entire changed-thread set of
a history window in a single isolate pass — `syncGmailMailboxIncremental`
collected every changed thread id and `getThread()`'d all of them into one
in-memory array ("a single pass drains the whole history window"). For a
large window (e.g. the history-cursor reseed after the 2026-06-25 Google
composite re-home, or a high-volume burst) this loaded hundreds of full
threads at once and exceeded the Cloudflare Worker memory limit.
The OOM killed the isolate mid-save: the API worker's request-scoped Kysely
connection was torn down in its `finally` while in-flight saveLink/createLink
work still queried it, surfacing as "driver has already been destroyed"
(PostHog 019f1398 / 019f1399) and dropping mail; the fetch storm also tripped
Gmail's per-minute API quota.
Bound each pass to MAX_INCREMENTAL_THREADS_PER_BATCH (20, matching the
initial-backfill page size). Overflow is returned as `deferredThreadIds`,
carried forward in `pendingThreadIds` WITHOUT bumping the failed-fetch attempt
counter (they were postponed, not tried), and drained on a self-terminating
continuation via the existing `queueIncrementalSync` scheduler hook. The
self-heal path is bounded the same way. retryThreadIds sort first, so prior
backlog drains ahead of newly-changed threads. No connector-class changes —
the composite Google connector inherits the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit d6b466b into mainJun 29, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/gmail-incremental-sync-bound-oom branch June 29, 2026 18:09
Sign up for freeto 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.

1 participant

@KrisBraun
, '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

fix(gmail): bound incremental sync per pass to stop isolate OOM - #241

Merged
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom
Jun 29, 2026
Merged

fix(gmail): bound incremental sync per pass to stop isolate OOM#241
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

The mailbox-wide Gmail incremental sync drained the entire changed-thread set of a history window in a single isolate pass — syncGmailMailboxIncremental collected every changed thread id and getThread()'d all of them into one in-memory array (the comment even said "a single pass drains the whole history window").

For a large window — the history-cursor reseed after the 2026-06-25 Google composite re-home, or a high-volume burst — this loaded hundreds of full threads at once and exceeded the Cloudflare Worker memory limit (Worker exceeded memory limit., PostHog issue 019ed581).

The OOM killed the isolate mid-save: the API worker's request-scoped Kysely connection was torn down in its finally while in-flight saveLink/createLink work was still querying it, surfacing as driver has already been destroyed (PostHog 019f1398 / 019f1399) and dropping mail. The fetch storm also tripped Gmail's per-minute API quota (403s).

Evidence

For the affected user's connector, the per-user error timeline interleaves Worker exceeded memory limit. (bursts of 8–9), Gmail 403 Quota exceeded … Queries per minute per user, and driver has already been destroyed — the driver errors land immediately after each OOM burst.

Fix

Bound each incremental pass to MAX_INCREMENTAL_THREADS_PER_BATCH = 20 (matching the proven-safe initial-backfill page size):

  • syncGmailMailboxIncremental fetches at most the cap and returns the overflow as deferredThreadIds. retryThreadIds are inserted first, so prior backlog drains ahead of newly-changed threads.
  • mergePendingThreads(prior, failed, deferred?) carries deferred ids forward without bumping the failed-fetch attempt counter — they were postponed, not tried — so a large backlog drains instead of being abandoned.
  • incrementalSyncBatchFn + the self-heal path carry deferred ids in pendingThreadIds and schedule a self-terminating continuation via the existing queueIncrementalSync scheduler hook (exit condition: nothing deferred; failed-only does not re-queue, so a permanently-unfetchable thread can't hot-loop).

No connector-class changes — the composite Google connector inherits the fix through the same shared functions.

Tests

  • New gmail-incremental-bound.test.ts (8 tests): per-pass cap + deferral, retry-id ordering, back-compat (unbounded when maxThreads omitted), deferred-carry without attempt bump, failed-vs-deferred distinction, and the incrementalSyncBatchFn cap + cursor-advance + continuation wiring.
  • Full suites pass: gmail (63), google (64). tsc + plot lint clean.

Connector-only change → no changeset.

🤖 Generated with Claude Code

The mailbox-wide incremental sync drained the entire changed-thread set of
a history window in a single isolate pass — `syncGmailMailboxIncremental`
collected every changed thread id and `getThread()`'d all of them into one
in-memory array ("a single pass drains the whole history window"). For a
large window (e.g. the history-cursor reseed after the 2026-06-25 Google
composite re-home, or a high-volume burst) this loaded hundreds of full
threads at once and exceeded the Cloudflare Worker memory limit.
The OOM killed the isolate mid-save: the API worker's request-scoped Kysely
connection was torn down in its `finally` while in-flight saveLink/createLink
work still queried it, surfacing as "driver has already been destroyed"
(PostHog 019f1398 / 019f1399) and dropping mail; the fetch storm also tripped
Gmail's per-minute API quota.
Bound each pass to MAX_INCREMENTAL_THREADS_PER_BATCH (20, matching the
initial-backfill page size). Overflow is returned as `deferredThreadIds`,
carried forward in `pendingThreadIds` WITHOUT bumping the failed-fetch attempt
counter (they were postponed, not tried), and drained on a self-terminating
continuation via the existing `queueIncrementalSync` scheduler hook. The
self-heal path is bounded the same way. retryThreadIds sort first, so prior
backlog drains ahead of newly-changed threads. No connector-class changes —
the composite Google connector inherits the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit d6b466b into mainJun 29, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/gmail-incremental-sync-bound-oom branch June 29, 2026 18:09
Sign up for freeto 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.

1 participant

@KrisBraun
, '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 > 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

fix(gmail): bound incremental sync per pass to stop isolate OOM - #241

Merged
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom
Jun 29, 2026
Merged

fix(gmail): bound incremental sync per pass to stop isolate OOM#241
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

The mailbox-wide Gmail incremental sync drained the entire changed-thread set of a history window in a single isolate pass — syncGmailMailboxIncremental collected every changed thread id and getThread()'d all of them into one in-memory array (the comment even said "a single pass drains the whole history window").

For a large window — the history-cursor reseed after the 2026-06-25 Google composite re-home, or a high-volume burst — this loaded hundreds of full threads at once and exceeded the Cloudflare Worker memory limit (Worker exceeded memory limit., PostHog issue 019ed581).

The OOM killed the isolate mid-save: the API worker's request-scoped Kysely connection was torn down in its finally while in-flight saveLink/createLink work was still querying it, surfacing as driver has already been destroyed (PostHog 019f1398 / 019f1399) and dropping mail. The fetch storm also tripped Gmail's per-minute API quota (403s).

Evidence

For the affected user's connector, the per-user error timeline interleaves Worker exceeded memory limit. (bursts of 8–9), Gmail 403 Quota exceeded … Queries per minute per user, and driver has already been destroyed — the driver errors land immediately after each OOM burst.

Fix

Bound each incremental pass to MAX_INCREMENTAL_THREADS_PER_BATCH = 20 (matching the proven-safe initial-backfill page size):

  • syncGmailMailboxIncremental fetches at most the cap and returns the overflow as deferredThreadIds. retryThreadIds are inserted first, so prior backlog drains ahead of newly-changed threads.
  • mergePendingThreads(prior, failed, deferred?) carries deferred ids forward without bumping the failed-fetch attempt counter — they were postponed, not tried — so a large backlog drains instead of being abandoned.
  • incrementalSyncBatchFn + the self-heal path carry deferred ids in pendingThreadIds and schedule a self-terminating continuation via the existing queueIncrementalSync scheduler hook (exit condition: nothing deferred; failed-only does not re-queue, so a permanently-unfetchable thread can't hot-loop).

No connector-class changes — the composite Google connector inherits the fix through the same shared functions.

Tests

  • New gmail-incremental-bound.test.ts (8 tests): per-pass cap + deferral, retry-id ordering, back-compat (unbounded when maxThreads omitted), deferred-carry without attempt bump, failed-vs-deferred distinction, and the incrementalSyncBatchFn cap + cursor-advance + continuation wiring.
  • Full suites pass: gmail (63), google (64). tsc + plot lint clean.

Connector-only change → no changeset.

🤖 Generated with Claude Code

The mailbox-wide incremental sync drained the entire changed-thread set of
a history window in a single isolate pass — `syncGmailMailboxIncremental`
collected every changed thread id and `getThread()`'d all of them into one
in-memory array ("a single pass drains the whole history window"). For a
large window (e.g. the history-cursor reseed after the 2026-06-25 Google
composite re-home, or a high-volume burst) this loaded hundreds of full
threads at once and exceeded the Cloudflare Worker memory limit.
The OOM killed the isolate mid-save: the API worker's request-scoped Kysely
connection was torn down in its `finally` while in-flight saveLink/createLink
work still queried it, surfacing as "driver has already been destroyed"
(PostHog 019f1398 / 019f1399) and dropping mail; the fetch storm also tripped
Gmail's per-minute API quota.
Bound each pass to MAX_INCREMENTAL_THREADS_PER_BATCH (20, matching the
initial-backfill page size). Overflow is returned as `deferredThreadIds`,
carried forward in `pendingThreadIds` WITHOUT bumping the failed-fetch attempt
counter (they were postponed, not tried), and drained on a self-terminating
continuation via the existing `queueIncrementalSync` scheduler hook. The
self-heal path is bounded the same way. retryThreadIds sort first, so prior
backlog drains ahead of newly-changed threads. No connector-class changes —
the composite Google connector inherits the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit d6b466b into mainJun 29, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/gmail-incremental-sync-bound-oom branch June 29, 2026 18:09
Sign up for freeto 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.

1 participant

@KrisBraun
, '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

fix(gmail): bound incremental sync per pass to stop isolate OOM - #241

Merged
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom
Jun 29, 2026
Merged

fix(gmail): bound incremental sync per pass to stop isolate OOM#241
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

The mailbox-wide Gmail incremental sync drained the entire changed-thread set of a history window in a single isolate pass — syncGmailMailboxIncremental collected every changed thread id and getThread()'d all of them into one in-memory array (the comment even said "a single pass drains the whole history window").

For a large window — the history-cursor reseed after the 2026-06-25 Google composite re-home, or a high-volume burst — this loaded hundreds of full threads at once and exceeded the Cloudflare Worker memory limit (Worker exceeded memory limit., PostHog issue 019ed581).

The OOM killed the isolate mid-save: the API worker's request-scoped Kysely connection was torn down in its finally while in-flight saveLink/createLink work was still querying it, surfacing as driver has already been destroyed (PostHog 019f1398 / 019f1399) and dropping mail. The fetch storm also tripped Gmail's per-minute API quota (403s).

Evidence

For the affected user's connector, the per-user error timeline interleaves Worker exceeded memory limit. (bursts of 8–9), Gmail 403 Quota exceeded … Queries per minute per user, and driver has already been destroyed — the driver errors land immediately after each OOM burst.

Fix

Bound each incremental pass to MAX_INCREMENTAL_THREADS_PER_BATCH = 20 (matching the proven-safe initial-backfill page size):

  • syncGmailMailboxIncremental fetches at most the cap and returns the overflow as deferredThreadIds. retryThreadIds are inserted first, so prior backlog drains ahead of newly-changed threads.
  • mergePendingThreads(prior, failed, deferred?) carries deferred ids forward without bumping the failed-fetch attempt counter — they were postponed, not tried — so a large backlog drains instead of being abandoned.
  • incrementalSyncBatchFn + the self-heal path carry deferred ids in pendingThreadIds and schedule a self-terminating continuation via the existing queueIncrementalSync scheduler hook (exit condition: nothing deferred; failed-only does not re-queue, so a permanently-unfetchable thread can't hot-loop).

No connector-class changes — the composite Google connector inherits the fix through the same shared functions.

Tests

  • New gmail-incremental-bound.test.ts (8 tests): per-pass cap + deferral, retry-id ordering, back-compat (unbounded when maxThreads omitted), deferred-carry without attempt bump, failed-vs-deferred distinction, and the incrementalSyncBatchFn cap + cursor-advance + continuation wiring.
  • Full suites pass: gmail (63), google (64). tsc + plot lint clean.

Connector-only change → no changeset.

🤖 Generated with Claude Code

The mailbox-wide incremental sync drained the entire changed-thread set of
a history window in a single isolate pass — `syncGmailMailboxIncremental`
collected every changed thread id and `getThread()`'d all of them into one
in-memory array ("a single pass drains the whole history window"). For a
large window (e.g. the history-cursor reseed after the 2026-06-25 Google
composite re-home, or a high-volume burst) this loaded hundreds of full
threads at once and exceeded the Cloudflare Worker memory limit.
The OOM killed the isolate mid-save: the API worker's request-scoped Kysely
connection was torn down in its `finally` while in-flight saveLink/createLink
work still queried it, surfacing as "driver has already been destroyed"
(PostHog 019f1398 / 019f1399) and dropping mail; the fetch storm also tripped
Gmail's per-minute API quota.
Bound each pass to MAX_INCREMENTAL_THREADS_PER_BATCH (20, matching the
initial-backfill page size). Overflow is returned as `deferredThreadIds`,
carried forward in `pendingThreadIds` WITHOUT bumping the failed-fetch attempt
counter (they were postponed, not tried), and drained on a self-terminating
continuation via the existing `queueIncrementalSync` scheduler hook. The
self-heal path is bounded the same way. retryThreadIds sort first, so prior
backlog drains ahead of newly-changed threads. No connector-class changes —
the composite Google connector inherits the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit d6b466b into mainJun 29, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/gmail-incremental-sync-bound-oom branch June 29, 2026 18:09
Sign up for freeto 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.

1 participant

@KrisBraun
, '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

fix(gmail): bound incremental sync per pass to stop isolate OOM - #241

Merged
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom
Jun 29, 2026
Merged

fix(gmail): bound incremental sync per pass to stop isolate OOM#241
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

The mailbox-wide Gmail incremental sync drained the entire changed-thread set of a history window in a single isolate pass — syncGmailMailboxIncremental collected every changed thread id and getThread()'d all of them into one in-memory array (the comment even said "a single pass drains the whole history window").

For a large window — the history-cursor reseed after the 2026-06-25 Google composite re-home, or a high-volume burst — this loaded hundreds of full threads at once and exceeded the Cloudflare Worker memory limit (Worker exceeded memory limit., PostHog issue 019ed581).

The OOM killed the isolate mid-save: the API worker's request-scoped Kysely connection was torn down in its finally while in-flight saveLink/createLink work was still querying it, surfacing as driver has already been destroyed (PostHog 019f1398 / 019f1399) and dropping mail. The fetch storm also tripped Gmail's per-minute API quota (403s).

Evidence

For the affected user's connector, the per-user error timeline interleaves Worker exceeded memory limit. (bursts of 8–9), Gmail 403 Quota exceeded … Queries per minute per user, and driver has already been destroyed — the driver errors land immediately after each OOM burst.

Fix

Bound each incremental pass to MAX_INCREMENTAL_THREADS_PER_BATCH = 20 (matching the proven-safe initial-backfill page size):

  • syncGmailMailboxIncremental fetches at most the cap and returns the overflow as deferredThreadIds. retryThreadIds are inserted first, so prior backlog drains ahead of newly-changed threads.
  • mergePendingThreads(prior, failed, deferred?) carries deferred ids forward without bumping the failed-fetch attempt counter — they were postponed, not tried — so a large backlog drains instead of being abandoned.
  • incrementalSyncBatchFn + the self-heal path carry deferred ids in pendingThreadIds and schedule a self-terminating continuation via the existing queueIncrementalSync scheduler hook (exit condition: nothing deferred; failed-only does not re-queue, so a permanently-unfetchable thread can't hot-loop).

No connector-class changes — the composite Google connector inherits the fix through the same shared functions.

Tests

  • New gmail-incremental-bound.test.ts (8 tests): per-pass cap + deferral, retry-id ordering, back-compat (unbounded when maxThreads omitted), deferred-carry without attempt bump, failed-vs-deferred distinction, and the incrementalSyncBatchFn cap + cursor-advance + continuation wiring.
  • Full suites pass: gmail (63), google (64). tsc + plot lint clean.

Connector-only change → no changeset.

🤖 Generated with Claude Code

The mailbox-wide incremental sync drained the entire changed-thread set of
a history window in a single isolate pass — `syncGmailMailboxIncremental`
collected every changed thread id and `getThread()`'d all of them into one
in-memory array ("a single pass drains the whole history window"). For a
large window (e.g. the history-cursor reseed after the 2026-06-25 Google
composite re-home, or a high-volume burst) this loaded hundreds of full
threads at once and exceeded the Cloudflare Worker memory limit.
The OOM killed the isolate mid-save: the API worker's request-scoped Kysely
connection was torn down in its `finally` while in-flight saveLink/createLink
work still queried it, surfacing as "driver has already been destroyed"
(PostHog 019f1398 / 019f1399) and dropping mail; the fetch storm also tripped
Gmail's per-minute API quota.
Bound each pass to MAX_INCREMENTAL_THREADS_PER_BATCH (20, matching the
initial-backfill page size). Overflow is returned as `deferredThreadIds`,
carried forward in `pendingThreadIds` WITHOUT bumping the failed-fetch attempt
counter (they were postponed, not tried), and drained on a self-terminating
continuation via the existing `queueIncrementalSync` scheduler hook. The
self-heal path is bounded the same way. retryThreadIds sort first, so prior
backlog drains ahead of newly-changed threads. No connector-class changes —
the composite Google connector inherits the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit d6b466b into mainJun 29, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/gmail-incremental-sync-bound-oom branch June 29, 2026 18:09
Sign up for freeto 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.

1 participant

@KrisBraun
, '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

fix(gmail): bound incremental sync per pass to stop isolate OOM - #241

Merged
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom
Jun 29, 2026
Merged

fix(gmail): bound incremental sync per pass to stop isolate OOM#241
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

The mailbox-wide Gmail incremental sync drained the entire changed-thread set of a history window in a single isolate pass — syncGmailMailboxIncremental collected every changed thread id and getThread()'d all of them into one in-memory array (the comment even said "a single pass drains the whole history window").

For a large window — the history-cursor reseed after the 2026-06-25 Google composite re-home, or a high-volume burst — this loaded hundreds of full threads at once and exceeded the Cloudflare Worker memory limit (Worker exceeded memory limit., PostHog issue 019ed581).

The OOM killed the isolate mid-save: the API worker's request-scoped Kysely connection was torn down in its finally while in-flight saveLink/createLink work was still querying it, surfacing as driver has already been destroyed (PostHog 019f1398 / 019f1399) and dropping mail. The fetch storm also tripped Gmail's per-minute API quota (403s).

Evidence

For the affected user's connector, the per-user error timeline interleaves Worker exceeded memory limit. (bursts of 8–9), Gmail 403 Quota exceeded … Queries per minute per user, and driver has already been destroyed — the driver errors land immediately after each OOM burst.

Fix

Bound each incremental pass to MAX_INCREMENTAL_THREADS_PER_BATCH = 20 (matching the proven-safe initial-backfill page size):

  • syncGmailMailboxIncremental fetches at most the cap and returns the overflow as deferredThreadIds. retryThreadIds are inserted first, so prior backlog drains ahead of newly-changed threads.
  • mergePendingThreads(prior, failed, deferred?) carries deferred ids forward without bumping the failed-fetch attempt counter — they were postponed, not tried — so a large backlog drains instead of being abandoned.
  • incrementalSyncBatchFn + the self-heal path carry deferred ids in pendingThreadIds and schedule a self-terminating continuation via the existing queueIncrementalSync scheduler hook (exit condition: nothing deferred; failed-only does not re-queue, so a permanently-unfetchable thread can't hot-loop).

No connector-class changes — the composite Google connector inherits the fix through the same shared functions.

Tests

  • New gmail-incremental-bound.test.ts (8 tests): per-pass cap + deferral, retry-id ordering, back-compat (unbounded when maxThreads omitted), deferred-carry without attempt bump, failed-vs-deferred distinction, and the incrementalSyncBatchFn cap + cursor-advance + continuation wiring.
  • Full suites pass: gmail (63), google (64). tsc + plot lint clean.

Connector-only change → no changeset.

🤖 Generated with Claude Code

The mailbox-wide incremental sync drained the entire changed-thread set of
a history window in a single isolate pass — `syncGmailMailboxIncremental`
collected every changed thread id and `getThread()`'d all of them into one
in-memory array ("a single pass drains the whole history window"). For a
large window (e.g. the history-cursor reseed after the 2026-06-25 Google
composite re-home, or a high-volume burst) this loaded hundreds of full
threads at once and exceeded the Cloudflare Worker memory limit.
The OOM killed the isolate mid-save: the API worker's request-scoped Kysely
connection was torn down in its `finally` while in-flight saveLink/createLink
work still queried it, surfacing as "driver has already been destroyed"
(PostHog 019f1398 / 019f1399) and dropping mail; the fetch storm also tripped
Gmail's per-minute API quota.
Bound each pass to MAX_INCREMENTAL_THREADS_PER_BATCH (20, matching the
initial-backfill page size). Overflow is returned as `deferredThreadIds`,
carried forward in `pendingThreadIds` WITHOUT bumping the failed-fetch attempt
counter (they were postponed, not tried), and drained on a self-terminating
continuation via the existing `queueIncrementalSync` scheduler hook. The
self-heal path is bounded the same way. retryThreadIds sort first, so prior
backlog drains ahead of newly-changed threads. No connector-class changes —
the composite Google connector inherits the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit d6b466b into mainJun 29, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/gmail-incremental-sync-bound-oom branch June 29, 2026 18:09
Sign up for freeto 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.

1 participant

@KrisBraun
, '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

fix(gmail): bound incremental sync per pass to stop isolate OOM - #241

Merged
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom
Jun 29, 2026
Merged

fix(gmail): bound incremental sync per pass to stop isolate OOM#241
KrisBraun merged 1 commit into
mainfrom
fix/gmail-incremental-sync-bound-oom

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

The mailbox-wide Gmail incremental sync drained the entire changed-thread set of a history window in a single isolate pass — syncGmailMailboxIncremental collected every changed thread id and getThread()'d all of them into one in-memory array (the comment even said "a single pass drains the whole history window").

For a large window — the history-cursor reseed after the 2026-06-25 Google composite re-home, or a high-volume burst — this loaded hundreds of full threads at once and exceeded the Cloudflare Worker memory limit (Worker exceeded memory limit., PostHog issue 019ed581).

The OOM killed the isolate mid-save: the API worker's request-scoped Kysely connection was torn down in its finally while in-flight saveLink/createLink work was still querying it, surfacing as driver has already been destroyed (PostHog 019f1398 / 019f1399) and dropping mail. The fetch storm also tripped Gmail's per-minute API quota (403s).

Evidence

For the affected user's connector, the per-user error timeline interleaves Worker exceeded memory limit. (bursts of 8–9), Gmail 403 Quota exceeded … Queries per minute per user, and driver has already been destroyed — the driver errors land immediately after each OOM burst.

Fix

Bound each incremental pass to MAX_INCREMENTAL_THREADS_PER_BATCH = 20 (matching the proven-safe initial-backfill page size):

  • syncGmailMailboxIncremental fetches at most the cap and returns the overflow as deferredThreadIds. retryThreadIds are inserted first, so prior backlog drains ahead of newly-changed threads.
  • mergePendingThreads(prior, failed, deferred?) carries deferred ids forward without bumping the failed-fetch attempt counter — they were postponed, not tried — so a large backlog drains instead of being abandoned.
  • incrementalSyncBatchFn + the self-heal path carry deferred ids in pendingThreadIds and schedule a self-terminating continuation via the existing queueIncrementalSync scheduler hook (exit condition: nothing deferred; failed-only does not re-queue, so a permanently-unfetchable thread can't hot-loop).

No connector-class changes — the composite Google connector inherits the fix through the same shared functions.

Tests

  • New gmail-incremental-bound.test.ts (8 tests): per-pass cap + deferral, retry-id ordering, back-compat (unbounded when maxThreads omitted), deferred-carry without attempt bump, failed-vs-deferred distinction, and the incrementalSyncBatchFn cap + cursor-advance + continuation wiring.
  • Full suites pass: gmail (63), google (64). tsc + plot lint clean.

Connector-only change → no changeset.

🤖 Generated with Claude Code

The mailbox-wide incremental sync drained the entire changed-thread set of
a history window in a single isolate pass — `syncGmailMailboxIncremental`
collected every changed thread id and `getThread()`'d all of them into one
in-memory array ("a single pass drains the whole history window"). For a
large window (e.g. the history-cursor reseed after the 2026-06-25 Google
composite re-home, or a high-volume burst) this loaded hundreds of full
threads at once and exceeded the Cloudflare Worker memory limit.
The OOM killed the isolate mid-save: the API worker's request-scoped Kysely
connection was torn down in its `finally` while in-flight saveLink/createLink
work still queried it, surfacing as "driver has already been destroyed"
(PostHog 019f1398 / 019f1399) and dropping mail; the fetch storm also tripped
Gmail's per-minute API quota.
Bound each pass to MAX_INCREMENTAL_THREADS_PER_BATCH (20, matching the
initial-backfill page size). Overflow is returned as `deferredThreadIds`,
carried forward in `pendingThreadIds` WITHOUT bumping the failed-fetch attempt
counter (they were postponed, not tried), and drained on a self-terminating
continuation via the existing `queueIncrementalSync` scheduler hook. The
self-heal path is bounded the same way. retryThreadIds sort first, so prior
backlog drains ahead of newly-changed threads. No connector-class changes —
the composite Google connector inherits the fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit d6b466b into mainJun 29, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/gmail-incremental-sync-bound-oom branch June 29, 2026 18:09
Sign up for freeto 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.

1 participant

@KrisBraun