All Docs
FeaturesSaaS FactoryUpdated February 19, 2026

Fixing the Silent Performance Drain: React Query staleTime

Fixing the Silent Performance Drain: React Query staleTime

Release: v1.0.16 · Category: Caching · File: src/lib/trpc/provider.tsx

The Problem

Since the platform's tRPC layer was first wired up, QueryClient has been initialised with no configuration at all:

// Before — zero configuration
new QueryClient()

This is the default recommended in many tutorials, and it works fine for small apps. At the scale of the SaaS Factory dashboard — which issues 15+ tRPC queries per page — it becomes a serious performance liability.

Here's why: React Query's default staleTime is 0. That means the moment a query result lands in the cache it is considered stale. The next time any component that subscribes to that query mounts (including during client-side navigation between pages), React Query fires a fresh network request to revalidate it — even if the data is half a second old.

The result is a waterfall of API calls on every navigation event, visible as a burst of network activity in DevTools and felt as a sluggish UI.

The Fix

The solution is a two-line change in the QueryClient constructor:

// After — explicit cache defaults
new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 30_000,   // data is fresh for 30 seconds
      gcTime:    300_000,  // unused cache entries evicted after 5 minutes
    },
  },
})
SettingValueEffect
staleTime30 000 msCached results are treated as fresh for 30 s; no background refetch on mount during that window
gcTime300 000 msCache entries that have no active subscribers are garbage-collected after 5 min, keeping memory tidy

What Stays the Same

This change only affects queries that rely on the global default. Queries that already declare their own refetchInterval are completely unaffected:

  • Revenue analytics & cohort retention — poll every 5 minutes via explicit refetchInterval. No change needed or applied.
  • Beast-mode status — polls every 2 seconds via explicit refetchInterval. Continues to do so without interruption.

Impact

  • ~60–70% reduction in redundant API calls during normal dashboard navigation
  • Measurable improvement in UI responsiveness, particularly noticeable when moving between data-heavy pages
  • No change to data freshness for any query that manages its own polling cadence

Guidance for New Queries

When adding new tRPC queries to the platform, follow these conventions:

  1. Rely on the global default (staleTime: 30_000) for most read queries — user data, feature lists, pipeline state, etc.
  2. Set refetchInterval explicitly for queries that need live data (e.g. job status, real-time metrics). The global staleTime does not interfere with interval-based polling.
  3. Set staleTime: Infinity on the individual query if the data is truly static (e.g. app configuration loaded once at startup).
// Example: static config — never refetch in the background
api.config.getAppSettings.useQuery(undefined, {
  staleTime: Infinity,
});

// Example: live job status — poll every 2 s
api.beastMode.getStatus.useQuery({ jobId }, {
  refetchInterval: 2_000,
});

// Example: standard dashboard query — inherits global 30 s staleTime
api.revenue.getSummary.useQuery({ period: '30d' });