# Duplicate network requests

> The same endpoint fetched several times wastes bandwidth and risks race conditions. Deduplicate with a query cache or a shared in-flight request.

_Category: Performance · Detector `duplicate-requests` · Severity: warning_

When several components each fetch the same endpoint, you pay for the same data multiple times — wasted bandwidth, extra server load, slower time-to-content, and the risk that responses arrive out of order and overwrite each other. It is endemic in AI-built frontends because each component is generated in isolation, so each one fetches what it needs independently instead of sharing.

## Symptoms

- The Network panel shows the same URL requested several times in a burst
- A list and its header both fetch the same resource on mount
- Occasional flicker or stale data from responses racing each other
- Server logs show duplicate reads for one page view

## How VibeCheck detects it

The `duplicate-requests` detector flags this live in the browser and reports it to the widget's Problems list — and to your coding agent over MCP.

- **Issue string:** `Duplicate GET request`
- **Threshold:** The same method + URL requested 2 or more times within 2 seconds

## Root causes

- Multiple components fetching the same endpoint independently on mount
- No request deduplication or client-side cache in front of the data layer
- An effect re-running on every render because its dependencies aren’t stable
- Rapid-fire calls from search/autocomplete without debouncing

## The fix

Put a caching data layer in front of your fetches so identical in-flight requests are deduplicated and results are shared. Lift shared data to a common parent, cache responses, debounce rapid-fire inputs, and set sensible `Cache-Control` headers on the API.

### Steps

1. Adopt a query cache (or a shared in-flight promise map) that dedupes by key
2. Give the same resource the same query key everywhere it’s used
3. Debounce search/autocomplete and cache responses on the server

_Framework-free: share the in-flight promise by key_

```js
const inFlight = new Map()
function dedupedFetch(url) {
  if (!inFlight.has(url)) {
    inFlight.set(url, fetch(url).then((r) => r.json()).finally(() => inFlight.delete(url)))
  }
  return inFlight.get(url)
}
```

## Framework-specific fixes

### React

TanStack Query (or SWR) deduplicates requests that share a query key automatically — ten components asking for the same key trigger one fetch and share the cached result.

```tsx
import { useQuery } from '@tanstack/react-query'

// Every component using this key shares ONE request + cache entry.
const { data } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) })
```

[React docs](https://tanstack.com/query/latest)

### Next.js

In Server Components, wrap the fetcher in React’s `cache()` so repeat calls in one render dedupe; native `fetch` is also automatically memoized per request. That removes duplicate reads without a client cache.

```tsx
import { cache } from 'react'
export const getUser = cache(async (id: string) => {
  const res = await fetch(`/api/users/${id}`)
  return res.json()
}) // called in many components → fetched once per request
```

[Next.js docs](https://nextjs.org/docs/app/deep-dive/caching#request-memoization)

### Vue

Use `@tanstack/vue-query` for the same key-based dedup, or hoist the fetch into a Pinia store so components read shared state instead of refetching.

```vue
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
const { data } = useQuery({ queryKey: ['user', props.id], queryFn: () => fetchUser(props.id) })
</script>
```

[Vue docs](https://tanstack.com/query/latest/docs/framework/vue/overview)

### Svelte

In SvelteKit, fetch shared data once in a load function and let child routes read it, rather than fetching in each component. load’s fetch also dedupes and can be cached.

_+page.ts — one load, shared down the tree_

```ts
export async function load({ fetch, params }) {
  const user = await fetch(`/api/users/${params.id}`).then((r) => r.json())
  return { user }
}
```

[Svelte docs](https://svelte.dev/docs/kit/load)

### Vanilla JS

Keep a Map of in-flight promises keyed by URL so concurrent callers share one request, and cache resolved results with a short TTL.

```js
const cache = new Map()
export function getJSON(url) {
  if (!cache.has(url)) cache.set(url, fetch(url).then((r) => r.json()))
  return cache.get(url)
}
```

## FAQ

### Is fetching the same URL twice always a bug?

Not always — polling or intentional revalidation is fine. VibeCheck flags the same method+URL hitting 2+ times within 2 seconds, which almost always means two components fetched independently rather than sharing.

### Does HTTP caching solve this?

`Cache-Control` helps the browser reuse responses, but the requests are still made and can still race. Client-side dedup (a query cache or shared promise) prevents the duplicate calls in the first place.

### Why do my duplicates come in bursts on mount?

Because several components mount together and each kicks off its own fetch. Sharing one query key (or lifting the fetch to a parent/loader) collapses them into a single request.

## Related problems

- [Large JavaScript bundles](https://vibecheck.wcgw.fun/fix/large-javascript-bundles.md) — Performance
- [Console.log spam](https://vibecheck.wcgw.fun/fix/console-log-spam.md) — Performance
- [JavaScript memory leak](https://vibecheck.wcgw.fun/fix/memory-leak.md) — Performance
- [Content only renders with JavaScript](https://vibecheck.wcgw.fun/fix/content-requires-javascript.md) — AI readiness

---

Fix guide from VibeCheck — https://vibecheck.wcgw.fun/fix/duplicate-network-requests. Full site index for LLMs: https://vibecheck.wcgw.fun/llms.txt
