wagmi: Memory leak when using useBlockNumber({watch: true}) -- creates a new websocket subscription on every block until it runs out of memory

Describe the bug

Consider a simple hook like this which consumes the block number for invalidation:

export const useAccountData = () => {
  const queryClient = useQueryClient()
  const { address } = useAccount()
  const { data: blockNumber } = useBlockNumber({ chainId: activeChains.l2.id, watch: true })
  const {
    data: accountData,
    isLoading,
    queryKey,
  } = useReadContract({
    address: env.NEXT_PUBLIC_CONTRACT as `0x${string}`,
    abi: fooAbi,
    functionName: "fooFunction",
  })

  useEffect(() => {
    void queryClient.invalidateQueries({ queryKey })
  }, [blockNumber])

  return { accountData, isLoading }
}

When this is used just one time in the app and you look at the websocket traffic, on every single block event it’s creating a new subscription and never closes the old ones. If you have 2 components consuming the hook, it will start 2 subscriptions with every block, etc for each consumer.

In the following screenshot notice it first gets 3 subscription responses, then starts a new one, then gets 4, etc. I ensured that hook above was only called/active one time, and setting watch:false resulted in no websocket activity. Screenshot 2024-02-29 at 12 38 26 PM

The result is you will run out of memory and the RPC will rate limit the client because eventually it has too many subscriptions.

I tested this with QuickNode and the following:

"@web3modal/wagmi": "^4.0.11",

I tried the same pattern but in a context and it’s the same issue. The problem seems to be wagmi just keeps making new subscriptions.

If I instead use viem to get the block number, it works fine and only starts one subscription, even if I have several parts of the app consuming this hook for the block number:

export const useOnL2Block = () => {
  const [blockNumber, setBlockNumber] = useState(BigInt(0))

  const subscribe = useCallback(() => {
    return watchBlockNumber(config.getClient(), {
      onBlockNumber: (blockNumber) => {
        setBlockNumber(blockNumber)
      },
    })
  }, [])

  useEffect(() => {
    const unsubscribe = subscribe()

    return () => unsubscribe()
  }, [subscribe])

  return { blockNumber }
}

export const useAccountData = () => {
  const queryClient = useQueryClient()
  const { address } = useAccount()

  const { blockNumber } = useOnL2Block()

  // const { data: blockNumber } = useBlockNumber({ chainId: activeChains.l2.id, watch: watch })
  const {
    data: accountData,
    isLoading,
    queryKey,
  } = useReadContract({
    address: env.NEXT_PUBLIC_CONTRACT as `0x${string}`,
    abi: fooAbi,
    functionName: "fooFunction",
  })

  useEffect(() => {
    void queryClient.invalidateQueries({ queryKey })
  }, [blockNumber])

  return { accountData, isLoading }
}
Screenshot 2024-02-29 at 12 54 16 PM

As another side note, what’s the correct pattern to ensure there’s only one bock subscription in the app so i’m not making unnecessary rpc requests?

Link to Minimal Reproducible Example

No response

Steps To Reproduce

No response

Wagmi Version

2.6.5

Viem Version

2.7.16

TypeScript Version

5.3.3

Check existing issues

Anything else?

No response

About this issue

  • Original URL
  • State: closed
  • Created 4 months ago
  • Reactions: 1
  • Comments: 24 (11 by maintainers)

Commits related to this issue

Most upvoted comments

@Yuripetusko @acoutts I just tested this branch with instructions above and I can confirm there is only 1 web socket being created.

I don’t see any easier way to handle this. Only kind of strange this is that when I print the result inside of use effect, it will print a function printed first, then undefined. Given

  useEffect(() => {
    console.log('inside use effect')
    console.log(subscription.current)
    return () => {
      subscription.current?.()
      subscription.current = undefined
    }
  }, [])

I see this printed in the network console:

image

Are there any obvious edge cases we should be aware of here? Testing what happens when there are 2 subscriptions to useWatchBlockNumber seems like a good test case.

@Yuripetusko @acoutts I just tested this branch with instructions above and I can confirm there is only 1 web socket being created.

I don’t see any easier way to handle this. Only kind of strange this is that when I print the result inside of use effect, it will print a function printed first, then undefined. Given

  useEffect(() => {
    console.log('inside use effect')
    console.log(subscription.current)
    return () => {
      subscription.current?.()
      subscription.current = undefined
    }
  }, [])

I see this printed in the network console:

image

Are there any obvious edge cases we should be aware of here? Testing what happens when there are 2 subscriptions to useWatchBlockNumber seems like a good test case.

Whoops, not sure how this console.log got there. WIll check

Let’s move to PR to discuss this, yes there’s one test case to test. I’ll describe there in a sec

I’ll have a look

@acoutts I created a PR with a potential fix, not super happy with amount of code I had to add, as i’m not familiar with codebase just yet, but if you could check out the branch and test that would be great

  1. edit playgrounds/next/src/wagmi.ts to add wss transport. ie.
[sepolia.id]: webSocket('wss://eth-sepolia.api.onfinality.io/public-ws'),
  1. add to playgrounds/next/src/app/page.tsx
const { data: blockNumber } = useBlockNumber({
    chainId: sepolia.id,
    watch: true,
  })

  console.log('blockNumber', blockNumber)

Launch with pnpm dev:next and check the network tab

Is your View instance set to use polling by any chance @acoutts ?

Because Viem’s watchBlockNumber is configured to use observer correctly, but the connection version with WebSocket is not. See https://github.com/wevm/viem/blob/main/src/actions/public/watchBlockNumber.ts

My best guess right now is that this is actually a Viem issue