
Request Deduplication: Stop Sending Duplicate Network Requests
Keywords:
Your React application just mounted. Five components initialize simultaneously. Each one needs the same user profile data. Without your knowledge, your browser sends five identical requests to the server.
This happens constantly in real applications, and most developers never notice it.
Request deduplication is the answer. It's a technique that consolidates identical requests into one, shares the response, and eliminates unnecessary network traffic.
If you're not using it, you're wasting bandwidth and server resources. If you're using React Query or SWR, it's already working behind the scenes. But understanding how it works—and when it works—is critical for building performant applications.
What Request Deduplication Actually Is
Request deduplication is a technique where the application detects when multiple code paths request the same data and consolidates them into a single network request. When the response arrives, all requesters receive the same data.
In technical terms: A request cache that prevents duplicate in-flight requests within a deduplication window.
In practical terms: "One request, many receivers."
Why This Matters
Every network request has costs:
- Bandwidth: Data traveling across the network
- Server resources: CPU, database queries, memory
- Battery: On mobile devices, network requests drain battery faster than computation
- Latency: Added wait time for responses
If you send five identical requests instead of one, you multiply all these costs by five.
For high-traffic applications, this isn't theoretical—it's the difference between a system that scales and one that collapses under load.
The Problem: Why Duplicates Happen
Real Scenario: The Dashboard Problem
Imagine you have a dashboard showing user information. Multiple components need the same data:
1// Header component
2const Header = () => {
3 const { data: user } = useQuery({
4 queryKey: ['user'],
5 queryFn: fetchUser
6 })
7 return <div>{user.name}</div>
8}
9
10// Sidebar component
11const Sidebar = () => {
12 const { data: user } = useQuery({
13 queryKey: ['user'],
14 queryFn: fetchUser
15 })
16 return <div>{user.avatar}</div>
17}
18
19// Profile card component
20const ProfileCard = () => {
21 const { data: user } = useQuery({
22 queryKey: ['user'],
23 queryFn: fetchUser
24 })
25 return <div>{user.email}</div>
26}
27
28// Dashboard mounts all three
29export const Dashboard = () => {
30 return (
31 <div>
32 <Header />
33 <Sidebar />
34 <ProfileCard />
35 </div>
36 )
37}Without deduplication, the browser sends three separate requests:
GET /api/user → Header component requests it
GET /api/user → Sidebar component requests it (while first is in flight)
GET /api/user → ProfileCard component requests it (while first is still in flight)
All three requests are identical. All three hit the server. All three consume bandwidth and server resources.
Why This Happens
The problem occurs because:
- Components are independent: Each component only knows about its own data needs
- Parallel rendering: React renders components concurrently; they often request data at the same time
- No built-in coordination: Without a caching layer, there's no mechanism to say "wait, someone else already asked for this"
This compounds in complex applications. A page with 10 components might send 10 identical requests for user data. A page with nested components and multiple data fetches might send dozens of duplicate requests per navigation.
The Solution: Request Deduplication
How It Works
Request deduplication works by detecting in-flight requests and sharing their results.
When the first component requests user data:
1. Header requests /api/user
2. Deduplication cache checks: "Is this request already in flight?"
3. Answer: "No"
4. Start request, store it in cache
When the second component requests the same data (before the first request completes):
1. Sidebar requests /api/user
2. Deduplication cache checks: "Is this request already in flight?"
3. Answer: "Yes, Header already requested it"
4. Return the existing in-flight request (don't create a new one)
When the response arrives:
1. Request completes
2. Response is cached
3. Both Header and Sidebar receive the same response
4. Both render with identical data
The Result
Without Deduplication:GET /api/user → Header
GET /api/user → Sidebar
GET /api/user → ProfileCard
(3 identical requests to server)
With Deduplication:GET /api/user → (shared by Header, Sidebar, ProfileCard)
(1 request to server, 3 recipients)
Request Deduplication in React Query
React Query handles deduplication automatically. You don't have to do anything special—it's built-in.
How React Query Deduplicates
React Query maintains a query cache indexed by query key. When you call useQuery with the same queryKey:
- Same render cycle: If multiple components request the same key within the same render cycle, React Query sends one request and shares the response
- Deduplication interval: React Query has a configurable deduplication window (default 0ms, meaning same render cycle only)
- Query cache: The response is cached and subsequent requests get the cached value
Basic Example
// This is all you needconst { data: user } = useQuery({
queryKey: ['user'],
queryFn: fetchUser
})
React Query automatically handles deduplication. All components using queryKey: ['user'] will:
- Receive the same response
- Only trigger one network request (during the same render cycle)
- Share cached results on subsequent renders
Detailed Example: Real Dashboard
Here's a production-ready example showing deduplication in action:
1import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query'
2
3const queryClient = new QueryClient()
4
5// Fetch function
6const fetchUser = async () => {
7 console.log('📡 Fetching user data...')
8 const response = await fetch('/api/user')
9 return response.json()
10}
11
12// Header component
13const Header = () => {
14 const { data: user, isLoading } = useQuery({
15 queryKey: ['user'], // Same key as other components
16 queryFn: fetchUser,
17 staleTime: 5 * 60 * 1000 // Cache for 5 minutes
18 })
19
20 if (isLoading) return <div>Loading...</div>
21 return <header>{user.name}</header>
22}
23
24// Sidebar component
25const Sidebar = () => {
26 const { data: user, isLoading } = useQuery({
27 queryKey: ['user'], // Same key—triggers deduplication
28 queryFn: fetchUser,
29 staleTime: 5 * 60 * 1000
30 })
31
32 if (isLoading) return <div>Loading...</div>
33 return <aside><img src={user.avatar} /></aside>
34}
35
36// Profile component
37const Profile = () => {
38 const { data: user, isLoading } = useQuery({
39 queryKey: ['user'], // Same key
40 queryFn: fetchUser,
41 staleTime: 5 * 60 * 1000
42 })
43
44 if (isLoading) return <div>Loading...</div>
45 return <div>{user.email}</div>
46}
47
48// Dashboard component
49const Dashboard = () => {
50 return (
51 <div>
52 <Header />
53 <Sidebar />
54 <Profile />
55 </div>
56 )
57}
58
59// App wrapper
60export const App = () => (
61 <QueryClientProvider client={queryClient}>
62 <Dashboard />
63 </QueryClientProvider>
64)When this component tree renders:
Header,Sidebar, andProfileall mount simultaneously- All three call
useQuerywithqueryKey: ['user'] - React Query detects that the first request is already in flight
- It returns the same in-flight request to all three components
- Result: 1 network request instead of 3
Console output:
📡 Fetching user data... (only logged once!)
If you removed deduplication and made three separate requests, you'd see:
📡 Fetching user data...
📡 Fetching user data...
📡 Fetching user data...
When Deduplication Works (and When It Doesn't)
✅ Deduplication Works
- Same render cycle: Components requesting within the same render
- Same query key: Identical
queryKeyvalues - Request in flight: Only while the first request is pending
- Exact same parameters: Query keys including all variables must match
// These deduplicate (same key):
useQuery({ queryKey: ['user'], queryFn: fetchUser })
useQuery({ queryKey: ['user'], queryFn: fetchUser })
// These DON'T deduplicate (different keys):
useQuery({ queryKey: ['user', 'name'], queryFn: fetchUser })
useQuery({ queryKey: ['user'], queryFn: fetchUser })
❌ Deduplication Doesn't Work
- Different query keys: Different keys = separate requests
- After response cached: Once data is cached and stale, deduplication doesn't apply—it uses the cache
- Different component trees: Requests in completely separate unmounted components don't deduplicate
- Sequential mounting: If components mount at different times (microseconds apart), deduplication may not trigger
// Doesn't deduplicate:
setTimeout(() => {
useQuery({ queryKey: ['user'], queryFn: fetchUser })
}, 100) // Different render cycle
// Doesn't deduplicate:
useQuery({ queryKey: ['user', { id: 1 }], queryFn: fetchUser })
useQuery({ queryKey: ['user', { id: 2 }], queryFn: fetchUser }) // Different user IDs
The Deduplication Window
React Query's default deduplication interval is 0ms, meaning only requests in the exact same render cycle deduplicate.
You can customize this:
1const queryClient = new QueryClient({
2 defaultOptions: {
3 queries: {
4 // All requests within 5 seconds of each other deduplicate
5 // (even if in different render cycles)
6 dedupeInterval: 5000
7 }
8 }
9})A wider deduplication window catches more duplicate requests, but it's less precise. A request that started 4.9 seconds ago might return stale data.
Best practice: Use the default (0ms) for synchronous rendering, and increase it only if you have requests that explicitly need it (like lazy loading or async component loading).
Real-World Scenarios
Scenario 1: Dashboard Loading
A dashboard with multiple panels, each showing user stats:
1// Each panel requests the same /api/stats endpoint
2const Panel1 = () => useQuery({ queryKey: ['stats'], queryFn: fetchStats })
3const Panel2 = () => useQuery({ queryKey: ['stats'], queryFn: fetchStats })
4const Panel3 = () => useQuery({ queryKey: ['stats'], queryFn: fetchStats })
5
6// Result: 1 request instead of 3Scenario 2: Nested Components
A page with deeply nested components that all need the same user info:
1const Page = () => (
2 <Layout>
3 <Header /> {/* requests user */}
4 <MainContent>
5 <Sidebar /> {/* requests user */}
6 <Article>
7 <AuthorBio /> {/* requests user */}
8 </Article>
9 </MainContent>
10 </Layout>
11)
12
13// All three make the same request
14// React Query: 1 network request
15// Without dedup: 3 network requestsScenario 3: Multiple Routes/Layouts
Your app has multiple route layouts that all show the current user:
1// Each route might have its own layout component
2const DashboardLayout = () => {
3 const user = useQuery({ queryKey: ['user'], queryFn: fetchUser })
4 // ... render dashboard
5}
6
7const EditorLayout = () => {
8 const user = useQuery({ queryKey: ['user'], queryFn: fetchUser })
9 // ... render editor
10}
11
12// When switching routes, if both render in the same cycle:
13// React Query deduplicates to 1 requestCommon Mistakes
❌ Using Different Keys for the Same Data
1// Bad: Creates duplicate requests
2const user1 = useQuery({ queryKey: ['currentUser'], queryFn: fetchUser })
3const user2 = useQuery({ queryKey: ['me'], queryFn: fetchUser })
4const user3 = useQuery({ queryKey: ['profile'], queryFn: fetchUser })
5
6// All three send separate requests because keys differ
7
8
9//Better: Use a single consistent key:
10
11const user1 = useQuery({ queryKey: ['user'], queryFn: fetchUser })
12const user2 = useQuery({ queryKey: ['user'], queryFn: fetchUser })
13const user3 = useQuery({ queryKey: ['user'], queryFn: fetchUser })
14
15// All deduplicate to 1 request❌ Forgetting Query Variables in Keys
1// Bad: Deduplicates incorrectly
2const user1 = useQuery({
3 queryKey: ['user'], // Missing userID
4 queryFn: () => fetchUser(userId1)
5})
6
7const user2 = useQuery({
8 queryKey: ['user'], // Same key, but different user!
9 queryFn: () => fetchUser(userId2)
10})
11
12// These deduplicate to 1 request, but they're fetching DIFFERENT users!
13Better: Include variables in the key:
1const user1 = useQuery({
2 queryKey: ['user', userId1],
3 queryFn: () => fetchUser(userId1)
4})
5
6const user2 = useQuery({
7 queryKey: ['user', userId2],
8 queryFn: () => fetchUser(userId2)
9})
10
11// Different keys = separate requests (correct!)Monitoring Deduplication
You can log when requests are deduplicated to understand what's happening:
1import { useQuery } from '@tanstack/react-query'
2
3const fetchUser = async () => {
4 console.log('📡 Fetching user (actual network request)') // Logged once
5 return await fetch('/api/user').then(r => r.json())
6}
7
8export const MyComponent = () => {
9 const { data } = useQuery({
10 queryKey: ['user'],
11 queryFn: fetchUser
12 })
13
14 return <div>{data?.name}</div>
15}If three components use this hook simultaneously, the console shows:
📡 Fetching user (actual network request) // Logged once, not three times
The hook is called three times, but fetchUser only executes once.
Key Takeaways
- Request deduplication prevents multiple identical requests from being sent simultaneously
- React Query does this automatically—you just need to use the same
queryKey - One rule: Keep query keys consistent for the same data
- The benefit: Saves bandwidth, reduces server load, improves performance
- It only works during the same render cycle for in-flight requests
For most applications using React Query, deduplication happens silently in the background. But understanding how it works helps you recognize when it's working (or not) and write more efficient applications.
Resources
- React Query Documentation: Queries — Official guide to query deduplication and caching
- MDN: Fetch API — Understanding network requests under the hood
- MDN: Cache-Control HTTP Headers — Related: browser-level caching strategies
- React Query: Query Keys — How to structure query keys effectively

