Scaling 1,000+ Item Syncs to Sitecore XM Cloud with p-limit in Next.js
Thursday, June 4, 2026
The Use Case
If you've tried firing hundreds of requests at once, you've probably seen 429 errors, dropped requests, or long-running processes crash. The fix isn't just "slower" - it's controlled concurrency.
This post shows how to use p-limit to reliably sync ~1,000 items from a Next.js app into Sitecore XM Cloud-with batching, retries, and backoff.
Why p-limit?
p-limit gives you a simple way to say:
"Run many async tasks, but only N at a time."
Instead of launching 1,000 requests in parallel, you cap concurrency (e.g., 5-10). That alone dramatically reduces throttling and improves success rates.
But p-limit shines when combined with:
Batching - process items in chunks
Retries with exponential backoff
Small delays between batches
The Target Pattern
Batch → Limit Concurrency → Retry → Pause → Repeat
Batch size: 25–100
Concurrency: 3–5 (start here)
Catch all failed items and retry on failed
Delay: 200–500ms between batches
Step 1: Install and set up p-limit
npm install p-limit
import pLimit from "p-limit";
const CONCURRENCY = 5;
const limit = pLimit(CONCURRENCY);const CONCURRENCY = 5; Defines that no more than 5 async tasks should run simultaneously.
const limit = pLimit(CONCURRENCY); Creates a function (limit) that wraps async operations and ensures that at most 5 are running at any given time.
Step 2: Retry with exponential backoff
Handle transient failures like 429 (rate limit) and 5xx:
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
const retryWithBackoff = async (
fn: () => Promise<any>,
retries = 3,
delay = 500
) => {
try {
return await fn();
} catch (err: any) {
const status = err?.response?.status;
if (retries > 0 && (status === 429 || status >= 500)) {
await sleep(delay);
return retryWithBackoff(fn, retries - 1, delay * 2);
}
throw err;
}
}; It tries to execute an async function (fn). If the operation fails because the server is overloaded or temporarily unavailable, it waits for some time and tries again. Each retry waits longer than the previous one. It handles Rate limiting (HTTP 429), Temporary server failures (HTTP 500, 502, 503, 504, etc.) and Unstable network conditions.
Step 3: Wrap your Sitecore call
Create a single function responsible for syncing one item:
const syncToSitecore = async (item: any) => {
return retryWithBackoff(async () => {
const res = await fetch("/api/sitecore-sync", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(item),
});
if (!res.ok) {
const error: any = new Error("Request failed");
error.response = { status: res.status };
throw error;
}
return res.json();
});
};
Step 4: Add batching + p-limit
Now combine batching with controlled concurrency:
const BATCH_SIZE = 50;
const processItems = async (items: any[]) => {
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batch = items.slice(i, i + BATCH_SIZE);
console.log(`Processing batch ${i / BATCH_SIZE + 1}`);
const tasks = batch.map(item =>
limit(() => syncToSitecore(item))
);
await Promise.all(tasks);
await sleep(300); // small pause to avoid throttling
}
};This code processes a large list of items and sends them to Sitecore in batches, while also limiting concurrency and adding retries. It
Splits the items into groups of 50.
Processes one group at a time.
Within each group, only 5 requests run simultaneously (because of your earlier p-limit(5)).
Retries failed requests using retryWithBackoff().
Waits 300 ms between batches.
Step 5: Add failure tracking (don’t skip this)
const failedItems: any[] = [];
const safeSync = async (item: any) => {
try {
return await syncToSitecore(item);
} catch (err) {
failedItems.push(item);
}
};
Then use:
const tasks = batch.map(item =>
limit(() => safeSync(item))
);
This lets you:
retry later
log errors
avoid silent data loss
Optional: Adaptive throttling
let dynamicDelay = 300;
if (res.status === 429) {
dynamicDelay += 200;
}
Then:
await sleep(dynamicDelay);
Where to run this code
You can run this in a .tsx component (e.g., useEffect or a button handler), but for 1,000+ items:
Better options
Next.js API route (/api/sync)
Background worker (Node, Azure Function, etc.)
Why
avoids browser limits
prevents UI blocking
more reliable for long-running jobs
What NOT to do
await Promise.all(items.map(syncToSitecore));This causes:
request spikes
throttling (429)
unstable syncs
Tuning guidelines
Start conservative:
Concurrency: 5
Batch size: 50
Delay: 300ms
Then adjust:
Increase concurrency if stable
Reduce if you see 429s
Monitor response times
Final Takeaway
For syncing large datasets into Sitecore XM Cloud:
p-limit is your concurrency guardrail—but batching, retry, and pacing make it production-ready.
If Used together, they give you:
high success rates
predictable performance
resilience under load