fix(blue-sdk-wagmi): remove blockNumber from query keys to prevent OOM#533
Merged
fix(blue-sdk-wagmi): remove blockNumber from query keys to prevent OOM#533
Conversation
Remove blockNumber and blockTag from all fetch*QueryKey functions so that
TanStack Query reuses a single cache entry per entity instead of creating
~2,200 new entries every block (~10s on Base L2).
Before: each block spawns N new Query objects, old ones fail to gc at scale,
memory grows ~2.3 MB/s → OOM in ~20 min on heavy markets (Base cbBTC/USDC).
After: queries are stable. blockNumber/blockTag are captured in the queryFn
closure and passed to the underlying viem fetch calls, so RPC reads still
target the correct block. Consumers drive re-fetches by calling
invalidateAllBlueSdkQueries(queryClient) on each new block.
Changes:
- queries/fetch*.ts: exclude blockNumber and blockTag from queryKey return;
capture them from the parameters closure for queryFn.
- hooks/use*.ts: remove staleTime: Infinity default when blockNumber is set.
Consumers can still pass staleTime via the query parameter.
- simulation-sdk-wagmi/useSimulationState: drop Infinity staleTime override.
- New export: invalidateAllBlueSdkQueries() + BLUE_SDK_QUERY_NAMES constant
for consumer-driven block-change invalidation.
BREAKING CHANGE: Query keys no longer contain blockNumber/blockTag.
Code that accesses the cache with block-specific keys
(queryClient.getQueryData(['fetchMarket', { ..., blockNumber }]))
must be updated. staleTime no longer defaults to Infinity when
blockNumber is provided.
The queryKey no longer contains blockNumber/blockTag, so spreading queryKey[1] into the viem fetch call caused a TS2322 type error (bigint | undefined not assignable to undefined). Fix: queryFn now captures the full parameters object from the closure and passes it directly to the viem fetch function. This is simpler, type-safe, and still passes blockNumber/blockTag correctly.
Per Romain's review: document that blockNumber/blockTag are intentionally excluded from query keys and note the keepPreviousData alternative for multi-block consumers.
Since query is destructured and spread (...query) into useQuery/useQueries, writing staleTime: query.staleTime is a no-op — it's already included via the spread. Remove the explicit line from all 20 hooks and the staleTime local variable from useSimulationState.
Rubilmax
previously approved these changes
Mar 25, 2026
With stable query keys (no blockNumber), TanStack Query won't automatically refetch when a new block is passed to useSimulationState. Add a useEffect that calls invalidateAllBlueSdkQueries(queryClient) whenever block.number changes. This ensures all queryFn closures re-execute with the updated blockNumber from the new parameters. Also fixes biome import ordering.
Use an InvalidatableQueryClient interface in invalidateAllBlueSdkQueries instead of importing QueryClient from @tanstack/query-core, which caused TS2345 when the simulation-sdk-wagmi package resolved a different minor version of query-core. Also adds block-change invalidation in useSimulationState using useQueryClient from @tanstack/react-query.
After rerender(newBlock), the useEffect that invalidates queries fires asynchronously. Wait for isFetchingAny to become truthy (invalidation triggered a refetch) before waiting for it to become falsy (refetch complete with fresh data).
With stable query keys (no blockNumber), TanStack Query won't refetch when the hook rerenders with a new block. Tests now share a queryClient with the hook via renderHook options and call invalidateAllBlueSdkQueries(queryClient) after each rerender to trigger the refetch. Also duck-types InvalidatableQueryClient in the helper to avoid @tanstack/query-core version coupling issues.
Rubilmax
previously approved these changes
Mar 25, 2026
Wire the shared prefix into every query key so that invalidateAllBlueSdkQueries can use a single prefix-based invalidation call instead of looping through individual names. Also migrate fetchMarketParams and fetchVaultConfig to the closure-based queryFn pattern for consistency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rubilmax
approved these changes
Mar 26, 2026
Foulks-Plb
approved these changes
Mar 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Remove
blockNumberandblockTagfrom allfetch*QueryKeyfunctions so that TanStack Query reuses a single cache entry per entity instead of creating ~2,200 new entries every block (~10 s on Base L2).The problem
On heavy market pages (e.g. Base cbBTC/USDC with 14 shared-liquidity vaults spanning ~35 markets), connecting a wallet creates ~2,200 TanStack queries. Because
blockNumberis part of every query key, each new block creates ~2,200 brand-new cache entries. TanStack Query's built-in GC does not reliably fire at this volume (observed 6,319 idle/inactive queries withgcTime: 30000that were never collected). Memory grows at ~2.3 MB/s → OOM in ~20 minutes.Consumer-side workarounds (manual cache cleanup, block-gating,
gcTime: 30_000) reduced growth to ~0.9 MB/s but cannot eliminate it — the V8 heap fragments from the sheer allocation/deallocation churn.The fix
Separate "what to cache by" from "what to fetch at."
fetch*QueryKeyblockNumber,blockTagchainIdonlyfetch*QueryOptions.queryFnqueryKey[1]staleTimeInfinitywhenblockNumbersetquery.staleTime)Changes
packages/blue-sdk-wagmi/src/queries/fetch*.ts— RemoveblockNumberandblockTagfromqueryKeyreturn. Capture them from theparametersclosure inqueryFn.packages/blue-sdk-wagmi/src/hooks/use*.ts(20 files) — RemovestaleTime: Infinitydefault whenblockNumber != null. Consumers passstaleTimeexplicitly if needed.packages/simulation-sdk-wagmi/src/hooks/useSimulationState.ts— Drop theInfinitystaleTime override; useparameters.query?.staleTimedirectly.packages/blue-sdk-wagmi/src/query-key-prefix.ts— ExportsBLUE_SDK_QUERY_NAMESandinvalidateAllBlueSdkQueries(queryClient)for consumer-driven block-change invalidation.Consumer migration
Existing consumer workarounds (manual cache cleanup
setInterval, skip-block-while-loading guard) can be removed after this lands.blockNumber/blockTag. Code that reads the cache with block-specific keys (queryClient.getQueryData(["fetchMarket", { ..., blockNumber }])) must be updated.staleTimeno longer defaults toInfinitywhenblockNumberis provided. Consumers relying on this implicit behavior should passstaleTimeexplicitly.Expected impact
Context
Full investigation: see attached
a0c1906d-ba4f-49d4-9817-884e500be386.mdSentry issues: MORPHO-CONSUMER-4HG (894 events), MORPHO-CONSUMER-32X (OOM)