Request Deduplication

Optimize API calls by automatically handling duplicate concurrent requests

Request deduplication prevents redundant API calls by identifying identical requests made while another is already running (in-flight). This saves bandwidth and prevents race conditions where overlapping requests finish out of order.

How it works

Deduplication only applies to active requests. Once a request completes, it is removed from the internal tracking cache.

  1. Task Queueing: Every request enters a tiny non-zero timeout slot. This ensures parallel calls (like Promise.all) execute sequentially so they can correctly detect each other in the cache.
  2. Keying: A unique dedupeKey is generated (customizable).
  3. Active Check: If a request with the same key is already running, CallApi applies the configured strategy.
  4. Cleanup: The key is automatically removed from the cache as soon as the request resolves or rejects.

Deduplication is client-instance based by default. Requests from different callApi instances won't deduplicate unless Global Scope is enabled.

Usage

Configure deduplication globally during client creation or override it per request.

1. Cancel Strategy (Default)

Aborts the previous in-flight request and starts the new one. Best for search-as-you-type or rapid navigation where only the latest query is relevant.

Cancel Strategy Visualization

search.ts
// Typing "hello" rapidly:
// "h", "he", "hel", "hell" are aborted. Only "hello" completes.
const { data } = await callApi("/api/search", {
	query: { q: "hello" },
	dedupeStrategy: "cancel",
});

2. Defer Strategy

Instead of starting a new fetch, the new call waits for the existing in-flight request and shares its promise. Ideal for loading initial config or data shared across multiple components.

Defer Strategy Visualization

api.ts
// Multiple components calling this at the same time share one HTTP request.
const { data } = await callApi("/api/app-config", {
	dedupeStrategy: "defer",
});

3. None Strategy

Disables tracking entirely. Every call triggers an independent request.

logs.ts
const { data } = await callApi("/api/logs", {
	dedupeStrategy: "none",
});

Custom Deduplication Key

The default dedupeKey is generated from the URL plus a stable hash of the Method, Body, and explicitly provided Headers.

Helpers like auth or automatic Content-Type detection are applied after the key is generated and do not affect the default deduplication key.

Static Key

Force unrelated requests to be treated as duplicates.

api.ts
const { data } = await callApi("/api/resource", {
	dedupeKey: "singleton-request",
});

Dynamic Key Callback

Use a function to return a key based on the request context.

api.ts
await callApi("/api/user/123", {
	dedupeKey: (ctx) => {
		const { method, fullURL } = ctx.options;

		// Ignore headers/body, only deduplicate by URL and Method
		return `${method}:${fullURL}`;
	},
});

Cache Scope

Controls whether the deduplication cache is isolated to a single client or shared globally.

Local Scope (Default)

Deduplication state is isolated to the client instance. Identical requests made from two different client instances will not deduplicate each other.

api.ts
const userClient = createFetchClient({ baseURL: "/users" });
const postClient = createFetchClient({ baseURL: "/posts" });
// These will not deduplicate with each other.

Global Scope

Multiple client instances share the same deduplication tracker. This allows requests from different client instances to deduplicate each other if they share the same keys.

api.ts
const userClient = createFetchClient({
	baseURL: "/api/users",
	dedupeCacheScope: "global",
});

const profileClient = createFetchClient({
	baseURL: "/api/profiles",
	dedupeCacheScope: "global",
});
// These will share deduplication state if they hit the same keys.

Scope Namespacing

Group specific clients together within the global scope using dedupeCacheScopeKey.

api.ts
const sharedScopeKey = "identity";

const authClient = createFetchClient({
	dedupeCacheScope: "global",
	dedupeCacheScopeKey: sharedScopeKey,
});

const accountClient = createFetchClient({
	dedupeCacheScope: "global",
	dedupeCacheScopeKey: sharedScopeKey, // Shares cache with authClient
});

Recommendations

  • Use cancel when you only need the most recent request (most common)
  • Use defer when multiple parts of your app need the same data simultaneously
  • Use none when requests must be independent (polling, etc.)

Types

Prop

Type

Edit on GitHub

Last updated on

On this page