Optimizing SWR Revalidation Intervals
SWR’s revalidation machinery gives fine-grained control over when the library re-fetches data, but misconfigured refreshInterval, dedupingInterval, revalidateOnFocus, and revalidateOnReconnect values are the most common source of network thrashing in production dashboards. This page is a recipe for diagnosing and fixing those misconfigurations, sitting under the Background Refetch Strategies cluster. For the broader context of how revalidation fits into server synchronization pipelines, see Cache Invalidation & Server Synchronization. If you need to co-ordinate these intervals with write operations, the Mutation Sync & Rollback cluster covers the write side.
Diagnostic Checklist
Before changing any configuration, confirm you are seeing at least one of these symptoms:
- Multiple identical
GETrequests appear in DevTools within milliseconds of each other (duplicate waterfall). - API gateway logs show a single client IP accounting for a disproportionate share of requests — a sign of a fixed-interval poll ignoring server capacity.
- The UI flickers or reverts to stale values while a mutation is in flight, indicating a background poll is overwriting optimistic state.
- Mobile sessions show abnormal battery drain even when the tab is backgrounded.
- A tab-focus event triggers 30+ concurrent
GETrequests across mounteduseSWRhooks.
How SWR’s Revalidation Timers Interact
Understanding the sequence in which SWR evaluates its revalidation options prevents the most common conflicts.
Every trigger — the interval timer, a focus event, a reconnect, or a manual mutate() call — passes through the same deduplication gate. If the previous request for that cache key completed less than dedupingInterval milliseconds ago, SWR discards the attempt and returns the cached value. This is why raising dedupingInterval below your API’s round-trip time fixes most duplicate-request waterfalls.
Step 1 — Fix duplicate requests with dedupingInterval
When you see it: Chrome DevTools shows 10–50 identical GET requests firing within a few hundred milliseconds when a list component mounts or a filter state changes.
Why it happens: Each useSWR('/api/metrics') instance starts its own deduplication timer. If dedupingInterval is shorter than the network round-trip, the second mount fires a new request before the first response arrives.
Steps
- Measure your API’s p95 round-trip in the Network panel. Note the value in milliseconds.
- Set
dedupingIntervalto at least that value — typically2000–5000ms for production APIs. - Normalize every cache key to a deterministic array so all hook instances share the same deduplication slot.
// Step 1 – Key normalization + raised dedupingInterval
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export function useMetrics(userId: string, filter: string) {
// Serialized array key → all hooks with identical args share one dedup slot
return useSWR(
['/api/metrics', userId, filter],
([url]) => fetcher(url),
{
dedupingInterval: 4000, // ms — set ≥ your p95 round-trip
keepPreviousData: true, // avoids layout shift during re-key
}
);
}
Cache Behavior Analysis: SWR serializes the array key with JSON.stringify and stores one in-flight promise in its internal deduplication map. Any useSWR call with the same serialized key that arrives within dedupingInterval ms of the first call attaches to that promise rather than creating a new fetch. When the promise resolves, every subscriber re-renders with the same data in a single synchronous batch.
Step 2 — Replace static refreshInterval with a function
When you see it: Mobile battery drain spikes during active sessions, or slow-network users see queued requests stacking up faster than they resolve.
Why it happens: A static refreshInterval: 5000 treats a 4G connection and a throttled 2G connection identically. On slow links, the next poll fires before the previous response arrives.
Steps
- Read
navigator.connection?.effectiveTypeto bucket the connection quality. - Return the appropriate delay from a function passed to
refreshInterval. SWR re-evaluates this function before scheduling each poll. - Optionally use the
dataargument — SWR v2 passes the last cached value — to widen the interval when the payload has not changed.
import useSWR from 'swr';
type NetworkInfo = EventTarget & { effectiveType?: '4g' | '3g' | '2g' | 'slow-2g' };
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export function useAdaptiveMetrics(endpoint: string) {
return useSWR(endpoint, fetcher, {
// SWR v2: refreshInterval can be a function receiving the latest data
refreshInterval: (latestData) => {
// Widen interval when data is stable (unchanged from previous poll)
const dataIsStale = latestData == null;
const conn = (navigator as any).connection as NetworkInfo | undefined;
const quality = conn?.effectiveType ?? '4g';
if (dataIsStale) return 2000; // first load — poll quickly
if (quality === '4g') return 5_000;
if (quality === '3g') return 15_000;
return 30_000; // slow-2g / unknown
},
dedupingInterval: 3000,
revalidateOnFocus: false,
keepPreviousData: true,
});
}
Cache Behavior Analysis: SWR evaluates the refreshInterval function each time the current poll completes and schedules the next timer. It does not restart the timer mid-flight. The latestData argument reflects the value currently in the SWR cache for this key, making it safe to compare with a previous snapshot without external state. If refreshInterval returns 0 or false, SWR cancels the timer entirely until the next mount.
Step 3 — Scope focus and reconnect revalidation per hook
When you see it: A single tab-focus event triggers dozens of concurrent GET requests, or re-enabling WiFi causes a burst of 429 responses.
Why it happens: revalidateOnFocus and revalidateOnReconnect default to true globally. When 40 useSWR hooks are mounted across a dashboard, every tab switch fires 40 simultaneous requests. Unlike stale-while-revalidate strategies that can serve cached content immediately, these revalidation bursts hit the origin synchronously.
Steps
- Disable
revalidateOnFocusandrevalidateOnReconnectat the globalSWRConfiglevel. - Add a single
visibilitychangelistener that callsmutate(key)only for the most critical data key, rather than re-enabling the flags on every hook. - Guard the manual
mutate()call with a ref that tracks active mutations to prevent overwriting optimistic state.
import useSWR, { mutate } from 'swr';
import { useRef, useEffect } from 'react';
/**
* Replaces SWR's native focus revalidation with a single, guarded listener.
* Ensures an in-flight mutation is never overwritten by a background refetch.
*/
export function useSafeRefetch<T>(
key: string,
fetcher: (key: string) => Promise<T>
) {
const isMutating = useRef(false);
const result = useSWR<T>(key, fetcher, {
revalidateOnFocus: false, // handled manually below
revalidateOnReconnect: false, // handled manually below
refreshInterval: 0, // polling managed by caller
dedupingInterval: 3000,
});
useEffect(() => {
const handleVisibility = () => {
if (!document.hidden && !isMutating.current) {
// Revalidate without resetting the cache optimistically
mutate(key);
}
};
document.addEventListener('visibilitychange', handleVisibility);
return () => document.removeEventListener('visibilitychange', handleVisibility);
}, [key]);
return { ...result, isMutating };
}
Cache Behavior Analysis: Calling mutate(key) without a data argument triggers a background revalidation: SWR keeps the cached value visible in the UI (no loading flash) while the new fetch completes, then replaces the cache entry atomically. Because isMutating.current gates the call, the listener is a no-op while a write is in-flight — preventing the stale-server-response-overwrites-optimistic-update race condition that commonly appears in dashboards using tag-based invalidation systems.
Edge Cases & Gotchas
Gotcha 1 — refreshInterval function captures a stale closure
Failure mode: The interval function reads a variable (e.g., a user preference) from the enclosing scope, but the closure is created once at hook instantiation and never updated.
Resolution: Move the interval logic outside the component and read dynamic values via a ref:
const qualityRef = useRef<string>('4g');
useEffect(() => {
const conn = (navigator as any).connection;
qualityRef.current = conn?.effectiveType ?? '4g';
conn?.addEventListener('change', () => {
qualityRef.current = conn.effectiveType ?? '4g';
});
}, []);
useSWR(key, fetcher, {
refreshInterval: () => qualityRef.current === '4g' ? 5000 : 20000,
});
Gotcha 2 — Memory leak from bypassing SWR’s timer with setInterval
Failure mode: A developer wraps mutate() in a manual setInterval to work around refreshInterval limitations. The interval accumulates on every mount because the cleanup is missing or the ref to the interval ID is not stable.
Resolution: Always prefer SWR’s native refreshInterval. If you must use a manual timer, store the interval ID in a stable useRef and clear it in the useEffect return:
useEffect(() => {
const id = setInterval(() => mutate(key), 10_000);
return () => clearInterval(id);
}, [key]);
Gotcha 3 — dedupingInterval does not apply across different SWRConfig providers
Failure mode: Two separate SWRConfig trees (e.g., a shell layout and a lazy-loaded module) each mount useSWR for the same URL. Because they use distinct SWR cache instances, the deduplication map is not shared and both fire network requests.
Resolution: Hoist SWRConfig to the application root or pass a shared cache provider instance:
import { SWRConfig, Cache } from 'swr';
const sharedCache: Cache = new Map();
export function AppRoot({ children }: { children: React.ReactNode }) {
return (
<SWRConfig value={{ provider: () => sharedCache, dedupingInterval: 4000 }}>
{children}
</SWRConfig>
);
}
Common Pitfalls & Resolutions
| Observable Issue | Root Cause | Diagnostic Resolution |
|---|---|---|
Tab focus triggers 30+ parallel GET requests |
revalidateOnFocus: true on every mounted hook fires simultaneously when visibilitychange fires |
Disable at SWRConfig level; replace with a single guarded mutate() call in a top-level listener |
| Background poll overwrites optimistic mutation, reverting the UI | refreshInterval fires while mutate(key, optimisticData) is pending; the resolved server response replaces the local state |
Guard the interval function with a isMutating ref; return 0 while any write is in-flight |
Duplicate waterfall despite dedupingInterval: 2000 |
Two SWRConfig providers create separate cache instances; deduplication maps are not shared |
Hoist SWRConfig to the application root and pass a single shared provider cache instance |
Frequently Asked Questions
How do I prevent SWR from refetching on every tab focus?
Set revalidateOnFocus: false globally in SWRConfig or per-hook. Then trigger revalidation explicitly by calling mutate(key) inside a visibilitychange listener, guarded so it only fires when no mutation is in-flight. This gives you the freshness guarantee of focus revalidation without the request burst.
What is the optimal dedupingInterval for high-traffic dashboards?
Start at 2000–5000 ms and align the value with your API’s p95 round-trip time. If your p95 latency is 800 ms, any dedupingInterval below 1000 ms will allow rapid component mounts to fire parallel requests before the first response arrives. Raise the interval until the duplicate waterfall disappears in the Network panel, then verify that UI update latency stays within acceptable bounds.
Can refreshInterval be a function rather than a static number?
Yes — SWR v2 accepts a function for refreshInterval. The function receives the latest cached data as its first argument and must return the next delay in milliseconds (or 0 / false to stop polling). This makes it straightforward to slow polling when the payload is unchanged or when navigator.connection.effectiveType reports a constrained network, without managing an external timer.
Related
- Background Refetch Strategies — the parent page covering adaptive polling, visibility-driven revalidation, and network-aware fetchers across React Query, SWR, and RTK Query.
- Stale-While-Revalidate Implementation — how to serve cached data immediately while a revalidation runs in the background, which pairs directly with the interval tuning on this page.
- Mutation Sync & Rollback — handling the write side so that in-flight mutations and background revalidation do not conflict.
- Cache Invalidation & Server Synchronization — the top-level guide to invalidation architecture, including tag-based, time-based, and event-driven strategies.