While tracing backend load back to the frontend on a production dashboard, I kept finding React Query hooks with staleTime: 0 — including on data that was push-driven.
Two things I internalized:
staleTime: 0 is not “sensible default caching” — it means refetch on every mount. Every navigation, every remount of a component using the hook, fires a network request. On an app shell where several always-mounted widgets each do this, every page transition becomes a burst of requests for data that hasn’t changed.
When freshness is push-driven, a long staleTime costs nothing. Our notification badges were updated by a websocket event: the server publishes on change, the client listens and calls invalidateQueries, and an invalidation refetches regardless of staleTime. Given that, the old 15-second staleTime bought zero liveness — a new notification already appeared instantly via the push path. All the short window did was add a refetch on nearly every navigation. We raised it to 5 minutes; badges still update the instant a notification arrives, on every device, and an entire category of background traffic disappeared. Same story for a projects list that went from staleTime: 0 to 60 seconds.
The question I now ask for every query: how does this data actually change, and how would the client find out? If the answer is “the server tells us,” set a long staleTime and let invalidation do its job. If the answer is “we poll by refetching,” make that a deliberate interval, not an accidental refetch-on-every-mount.
This mattered more than frontend hygiene usually does: on our serverless backend, every one of those redundant requests had a real memory cost. The frontend’s fetching discipline is backend capacity planning.