Proxy Retry Budgets for Node.js Fetch
October 26, 2026
Why Retry Budgets Matter Before Retrying Through a Proxy
Naive retry logic can turn a small proxy failure into a rate-limit cascade. When a Node.js fetch call through a rotating proxy fails with a timeout, a 502, or a 503, the default instinct is to retry immediately. Retrying feels harmless for idempotent GET requests, but through a proxy it can amplify load, exhaust proxy sessions, and confuse anti-bot systems.
A retry budget is a policy that limits how many times a logical operation may retry. It can be scoped by URL, by proxy, by session, by minute, or by operation ID. Without a budget, retries become unbounded and unpredictable.
What Can Go Wrong Without a Budget
- Immediate retries can trigger secondary rate limits.
- Retries can duplicate POST or PUT side effects.
- Proxy pools can become unbalanced.
- Backoff is absent, so failures overlap.
- Observability gaps hide where retries happen.
A proxy adds latency and failure modes. A timeout may mean the proxy is slow, the target is down, or the exit node is blocked. Retrying immediately may hit the same proxy again. With rotating proxies, you may get a fresh IP, but you may also get a shared exit node that is already stressed.
Building a Retry Budget in Node.js Fetch
Node.js 18 and later include global fetch, and the undici library provides ProxyAgent. A proxy-aware fetch wrapper should inject the proxy and track attempts. The wrapper should be small, but the policy logic matters more. You need to know which request failed, which proxy was used, and whether the operation is safe to retry. A scraper fetching product pages can retry safely. A checkout flow cannot.
Step 1: Create a Proxy-Aware Fetch Wrapper
Use undici ProxyAgent with global fetch. The dispatcher tells fetch which proxy to use. Create the dispatcher once per proxy pool or per session. Reuse it when possible to keep connection pooling effective. Do not create a new ProxyAgent for every request unless you need rotation.
Step 2: Add Retry State
Keep retry counts in a Map. Key by operation ID or URL. Apply a rolling window. Use a combination of method, URL, and body hash if operation IDs are not available. This prevents different requests from sharing retry counts.
Step 3: Implement Backoff and Retry
Use exponential backoff with jitter. Respect Retry-After for 429 and 503 responses. The backoff calculation should be capped. For example, delay equals min of 30 seconds and base times 2 to the power of attempt plus jitter. The retry loop should check the budget before each attempt. If the budget is exhausted, return the last error or trigger a custom fallback. Do not silently swallow failures. Surface the final error to the caller.
Choosing Retry and Backoff Strategies
A retry budget should be per proxy or per client. For rotating proxies, per proxy may help isolate noisy exit nodes. For sticky sessions, per session may be better. Start with conservative defaults. A retry budget is not a way to retry more. It is a way to retry smart.
Recommended Defaults
- Max retries: 2 to 3 for idempotent GETs.
- Max retries: 0 to 1 for POSTs without idempotency keys.
- Backoff: exponential, base 500ms, multiplier 2, jitter 0 to 500ms.
- Respect Retry-After for 429 and 503.
- Log every retry with proxy ID, URL, status, and delay.
Idempotency
For POST requests, use idempotency keys. Proxy rotation should not turn a retry into a duplicate order. Generate keys from operation ID and payload hash.
How Proxy Rotation Changes Retry Semantics
When you rotate proxies, each attempt may use a different exit node. That means a retry is not just a retry. It is a new network context. You should reset backoff per proxy or keep global budget. For high-volume scraping, a global retry budget prevents stampedes.
Proxy-Level Budgets
If you use rotating proxies, set max retries per proxy. This avoids one bad exit node causing repeated failures. After N retries on one proxy, mark it unhealthy.
Session-Level Budgets
For sticky sessions, keep budget per session. This avoids reusing a session that is blocked.
Real-World Example: E-commerce Price Tracking
Imagine tracking prices across regions. A fetch fails with 503. Retrying immediately may get through, but if the proxy pool is small, it may exhaust the pool. Use a retry budget keyed by product URL and region. If retries exceed budget, mark the product for later or switch proxy pool.
A better flow records the failure, sleeps with jitter, rotates proxy, and increments the budget. After the budget is exhausted, stop and alert. This prevents retry storms. For a price tracker, you may have thousands of operations. Retrying all of them at once can trigger anti-bot. Use per-operation budgets and staggered delays. In a distributed setup, each worker may have its own budget. Use a shared store for global limits. This is important when you run multiple scrapers against the same target.
Testing and Observability
Use nock or a local HTTP server to simulate 429 and 503. Verify backoff timing. Ensure proxy rotation does not bypass retry limits. Add structured logs. Record proxy ID, attempt number, status, error message, and next backoff. Use metrics to track retry rate. Alert on retry ratio. Run chaos tests. Kill proxy mid-request. Simulate 503. Verify budget stops retries. Check logs. Test retry behavior under proxy rotation. Change proxy between attempts and confirm backoff still applies. Verify that sticky sessions do not bypass budgets.
Common Pitfalls
- Retrying all methods.
- No jitter.
- No Retry-After handling.
- No per-proxy limits.
- No logging.
Conclusion
Retry budgets make proxy retries predictable. They protect targets and proxy pools. Start small, measure, then expand.