Skip to content

fix(blue-sdk-wagmi): remove blockNumber from query keys to prevent OOM#533

Merged
0xbulma merged 15 commits intomainfrom
feat/stable-query-keys
Mar 26, 2026
Merged

fix(blue-sdk-wagmi): remove blockNumber from query keys to prevent OOM#533
0xbulma merged 15 commits intomainfrom
feat/stable-query-keys

Conversation

@prd-carapulse
Copy link
Copy Markdown
Contributor

@prd-carapulse prd-carapulse bot commented Mar 25, 2026

Summary

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 (~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 blockNumber is 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 with gcTime: 30000 that 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."

Layer Before After
fetch*QueryKey includes blockNumber, blockTag entity identifiers + chainId only
fetch*QueryOptions.queryFn reads block from queryKey[1] captures block from closure
Hook staleTime defaults to Infinity when blockNumber set passthrough from consumer (query.staleTime)
Cache entries per cycle ~2,200 created + ~2,200 destroyed 0 new / 0 destroyed (re-fetched in place)
Memory growth (connected wallet) ~0.9 MB/s (with workarounds) ~0 (stable)

Changes

  • packages/blue-sdk-wagmi/src/queries/fetch*.ts — Remove blockNumber and blockTag from queryKey return. Capture them from the parameters closure in queryFn.
  • packages/blue-sdk-wagmi/src/hooks/use*.ts (20 files) — Remove staleTime: Infinity default when blockNumber != null. Consumers pass staleTime explicitly if needed.
  • packages/simulation-sdk-wagmi/src/hooks/useSimulationState.ts — Drop the Infinity staleTime override; use parameters.query?.staleTime directly.
  • New: packages/blue-sdk-wagmi/src/query-key-prefix.ts — Exports BLUE_SDK_QUERY_NAMES and invalidateAllBlueSdkQueries(queryClient) for consumer-driven block-change invalidation.

Consumer migration

import { invalidateAllBlueSdkQueries } from "@morpho-org/blue-sdk-wagmi";

// In the component that tracks blocks:
useEffect(() => {
  if (!block?.number) return;
  invalidateAllBlueSdkQueries(queryClient);
}, [block?.number]);

Existing consumer workarounds (manual cache cleanup setInterval, skip-block-while-loading guard) can be removed after this lands.

⚠️ Breaking changes

  • Query keys no longer contain blockNumber/blockTag. Code that reads the cache with block-specific keys (queryClient.getQueryData(["fetchMarket", { ..., blockNumber }])) must be updated.
  • staleTime no longer defaults to Infinity when blockNumber is provided. Consumers relying on this implicit behavior should pass staleTime explicitly.

Expected impact

Metric Before (with workarounds) After
Cache entries at steady state ~2,200 + stale accumulation ~2,200 (fixed)
Entries created per block ~2,200 0
Memory growth (wallet connected) ~0.9 MB/s ~0
Time to OOM on heavy markets ~60 min Never
Consumer workarounds needed 3 0

Context

Full investigation: see attached a0c1906d-ba4f-49d4-9817-884e500be386.md

Sentry issues: MORPHO-CONSUMER-4HG (894 events), MORPHO-CONSUMER-32X (OOM)

prd-carapulse bot added 6 commits March 25, 2026 11:33
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
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.
prd-carapulse bot added 7 commits March 25, 2026 14:28
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
Rubilmax previously approved these changes Mar 25, 2026
@0xbulma 0xbulma requested a review from Foulks-Plb March 26, 2026 08:56
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>
@0xbulma 0xbulma merged commit cdc0f34 into main Mar 26, 2026
16 checks passed
@0xbulma 0xbulma deleted the feat/stable-query-keys branch March 26, 2026 13:30
Sign up for free to 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.

3 participants