Skip to content

fix(vtex/fetchCache): timeout inflight Promise to evict zombie cache entries#74

Merged
JonasJesus42 merged 1 commit into
mainfrom
fix/inflight-cache-promise-leak
Jun 4, 2026
Merged

fix(vtex/fetchCache): timeout inflight Promise to evict zombie cache entries#74
JonasJesus42 merged 1 commit into
mainfrom
fix/inflight-cache-promise-leak

Conversation

@JonasJesus42

@JonasJesus42 JonasJesus42 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Wraps executeFetch calls in vtex/utils/fetchCache.ts with a 10s timeout so the .finally() cleanup always runs, even if the underlying VTEX fetch() never settles.
  • Without this, a hung subrequest (TCP open with no response, CDN holding the connection) leaked the inflight Map slot forever and every subsequent caller for the same cache key joined the zombie Promise — pinning request context in memory until exceededMemory.
  • Adds a regression test that simulates a never-settling fetch and verifies the inflight Map is evicted after the timeout.

Closes #73.

Production impact this resolves

Observed on a TanStack Start storefront over a 24h window:

Path Events exceededMemory Avg CPU Avg wall
/escolar 849 333 743ms 197s
/malas 20 20 183ms 1738s
/outlet 20 20 9ms 2064s
/mochilas 6 6 2ms 3846s

CPU near zero + wall time in the tens of minutes = isolates sleeping awaiting the zombie Promise. One route held 82% of events — consistent with a single poisoned cache key blocking every request for that URL.

Test plan

  • npx vitest run vtex/utils/__tests__/fetchCache.test.ts — 8/8 pass including new evicts the inflight slot when the fetch never settles case
  • npm run typecheck clean
  • Validate in staging: force a hung VTEX response, confirm getFetchCacheStats().inflight stays at 0 after the timeout

Related

Companion fix for the four equivalent inflight Maps in @decocms/start: decocms/deco-start#223 (PR forthcoming).


Summary by cubic

Adds a 10s timeout around fetchWithCache inflight requests to evict zombie cache entries when VTEX fetch() hangs, preventing memory leaks and blocked requests. Also applies to stale-refresh and includes a regression test.

  • Bug Fixes
    • Wrapped executeFetch with withTimeout (10s) so .finally() always runs and clears the inflight slot.
    • Applied the same timeout to background stale refresh to avoid refreshing staying true forever.
    • Added a test that simulates a never-settling fetch and verifies the inflight Map is evicted after the timeout.

Written for commit 914553a. Summary will update on new commits.

Review in cubic

…entries

Module-level `inflight: Map<string, Promise<CacheEntry>>` only evicted entries
via `.finally()` on the stored Promise. If the underlying `fetch()` never
settled (TCP connection dropped without FIN, CDN holds response open), the
`.finally()` never ran and the Map entry leaked forever — every subsequent
request for the same cache key joined the zombie Promise, pinning context
references in memory until `exceededMemory`.

Wrap `executeFetch` with a 10s timeout so `.finally()` always runs. The
underlying hung fetch is abandoned (CF runtime will GC it after the request
ends) but the cache slot is freed and the next request retries.

Observed production impact on a TanStack Start storefront (24h window):
- 514 hard exceededMemory crashes
- 403 canceled requests
- 371 HTTP 503s
- One PLP route accounted for 82% of events (single poisoned cache key)
- CPU near zero + wall_time in tens of minutes = isolates sleeping awaiting
  the zombie Promise, not computing

Closes #73
@JonasJesus42 JonasJesus42 requested a review from a team June 4, 2026 18:37
@JonasJesus42 JonasJesus42 merged commit 8bdd873 into main Jun 4, 2026
1 of 2 checks passed
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 4.0.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: inflight Map in vtex/utils/fetchCache.ts retains hung Promises (exceededMemory in prod)

1 participant