
Security News
RubyGems Adds Cooldown Feature to Bundler for Newly Published Gems
RubyGems and Bundler 4.0.13 introduced an opt-in cooldown feature that delays newly published gems during dependency resolution.
A production-grade, policy-driven TypeScript retry library.
retry-pro provides a structured, composable approach to retrying failed async operations. Instead of re-implementing retry logic per call site, it gives you a Policy-First model that is predictable, cancellable, observability-ready, and safe for production at scale.
Key properties:
npm install retry-pro
import { retry } from 'retry-pro';
const result = await retry(async (signal) => {
return await fetch('/api/data', { signal }).then(r => r.json());
}, { retries: 3 });
Always forward the
signalargument into your async operation. This enables clean cancellation and hedging.
Policies bundle a retry count, delay strategy, and retry condition together. Pass a policy name instead of configuring everything manually.
| Policy | Retries | Strategy | Retries On |
|---|---|---|---|
httpSafe | 3 | Exponential (1000ms base) | 5xx only |
networkOnly | 5 | Exponential (500ms base) | Network errors (ECONNRESET, ETIMEDOUT, etc.) |
aggressive | 10 | Fixed (100ms) | Everything |
safe | 2 | Exponential (2000ms base) | 5xx + network errors |
const data = await retry(fetchMyData, { policy: 'httpSafe' });
import { registerPolicy, exponentialStrategy, retry } from 'retry-pro';
registerPolicy('databaseFailover', {
retries: 3,
strategy: exponentialStrategy(500, 2, 10000), // base 500ms, factor 2, max 10s
shouldRetry: (err) => err.code === 'ECONNRESET'
});
await retry(queryDb, { policy: 'databaseFailover' });
Attempting to register a policy name that already exists will throw an error.
Strategies control how long to wait between attempts. All strategies cap at maxDelay (default: 30,000ms).
import { exponentialStrategy } from 'retry-pro';
// delay = base * (factor ^ attempt), capped at maxDelay
exponentialStrategy(baseDelay: number, factor?: number, maxDelay?: number)
// e.g. exponentialStrategy(1000) → 1000, 2000, 4000, 8000...
import { linearStrategy } from 'retry-pro';
// delay = base + (increment * attempt)
linearStrategy(baseDelay: number, increment: number, maxDelay?: number)
// e.g. linearStrategy(200, 100) → 200, 300, 400...
import { fixedStrategy } from 'retry-pro';
// delay = constant
fixedStrategy(delay: number, maxDelay?: number)
// e.g. fixedStrategy(500) → 500, 500, 500...
Wraps any strategy to add randomized variance, preventing thundering herd problems.
import { withJitter, exponentialStrategy } from 'retry-pro';
// delay = baseDelay + (baseDelay * jitterFactor * random)
withJitter(strategy, jitterFactor?: number, maxDelay?: number)
// e.g. withJitter(exponentialStrategy(1000), 0.2)
await retry(fn, {
retries: 3, // Max retry attempts
policy: 'httpSafe', // Built-in or custom policy name
strategy: fixedStrategy(500), // Delay strategy (overrides policy's)
shouldRetry: (err) => true, // Condition to retry (overrides policy's)
signal: controller.signal, // AbortSignal for cancellation
attemptTimeout: 5000, // Hard timeout per attempt (ms)
fallback: () => defaultValue, // Value or async fn when all retries fail
adaptive: true, // Adaptive delay based on error type
priority: 'high', // 'low' | 'normal' | 'high'
onRetry: async (ctx) => {}, // Hook before each retry
onSuccess: async (ctx) => {}, // Hook on success
onFailure: async (ctx) => {}, // Hook on final failure
});
All hooks receive a rich context object.
await retry(fn, {
retries: 3,
onRetry: async ({ attempt, strategyDelayMs, totalElapsedMs, error }) => {
console.warn(`Attempt ${attempt} failed. Retrying in ${strategyDelayMs}ms. Elapsed: ${totalElapsedMs}ms`);
},
onSuccess: async ({ attempt, totalElapsedMs, result }) => {
console.log(`Succeeded on attempt ${attempt}`);
},
onFailure: async ({ attempt, totalElapsedMs, error }) => {
console.error(`All retries exhausted after ${attempt} attempts`, error);
}
});
Pass an AbortSignal to cancel the entire retry loop, including any active delay intervals.
const controller = new AbortController();
// Cancel after 10 seconds
setTimeout(() => controller.abort(), 10000);
try {
await retry(fn, { signal: controller.signal, retries: 10 });
} catch (err) {
if (err instanceof AbortError) {
console.log('Operation was cancelled');
}
}
const result = await retry(fetchPrices, {
retries: 2,
attemptTimeout: 5000, // Reject any attempt taking > 5s
fallback: () => ({ cached: true, price: 50 }) // Returned after all retries fail
});
When adaptive: true, the delay strategy is determined automatically by inspecting the error:
| Error Type | Strategy |
|---|---|
| Rate limited (429) | Slow exponential (1000 * 3^attempt) or uses Retry-After header |
| Network error (ECONNRESET, ETIMEDOUT, etc.) | Fast incremental (100 * (attempt + 1)) |
| Everything else | Standard exponential (1000 * 2^attempt) |
await retry(apiCall, { adaptive: true });
Prevents cascading failures by stopping requests to a known-failing service. Uses a rolling failure window.
import { retry, CircuitBreaker } from 'retry-pro';
const circuit = new CircuitBreaker({
failureThreshold: 3, // Open after 3 failures
resetTimeout: 30000, // Try again after 30s (HALF_OPEN)
successThreshold: 2, // Require 2 successes to fully close
window: 60000 // Only count failures within last 60s
});
await retry(fetchUsers, { circuitBreaker: circuit });
States: CLOSED → OPEN → HALF_OPEN → CLOSED
CircuitBreakerOptions and an instance will be created for you.await retry(fn, {
circuitBreaker: { failureThreshold: 5, resetTimeout: 30000 }
});
Prevents retry storms by capping the total retries allowed across multiple calls in a rolling time window.
import { retry, RetryBudget } from 'retry-pro';
const budget = new RetryBudget({ maxRetries: 50, window: 60000 });
// All retry() calls sharing this budget instance will collectively
// be limited to 50 retries per 60 seconds.
await retry(queryCluster, { retryBudget: budget });
Methods available on RetryBudget:
budget.canRetry() // → boolean
budget.consume() // Atomically checks and decrements; returns boolean
budget.getStats() // → { remaining, resetIn, used }
budget.reset() // Manually reset the window
You can also supply options directly and an instance will be created internally:
await retry(fn, { retryBudget: { maxRetries: 50, window: 60000 } });
Launches a duplicate request if the original hasn't responded within a threshold, resolving with whichever completes first. Losers are automatically aborted.
await retry(async (signal) => fetch('/data', { signal }), {
hedging: {
enabled: true,
delay: 200, // Launch a hedge after 200ms
maxHedges: 1, // Max number of duplicate requests (default: 1)
concurrencyLimit: 20 // Global limit on simultaneous hedge requests
}
});
⚠️ Only use hedging on idempotent operations. It increases load on downstream services.
Automatically tunes retry count and delays without manual configuration.
| Priority | Retries | Delay |
|---|---|---|
high | +2 | ×0.5 (50% faster) |
normal | unchanged | unchanged |
low | −1 (min 0) | ×1.5 (50% slower) |
await retry(criticalAction, { priority: 'high' });
await retry(backgroundTask, { priority: 'low' });
import { AbortError, RetryError, CircuitOpenError } from 'retry-pro';
try {
await retry(fn, { retries: 3 });
} catch (err) {
if (err instanceof AbortError) { /* cancelled */ }
if (err instanceof RetryError) {
console.log(err.attempts); // How many attempts were made
console.log(err.lastError); // The underlying error
}
if (err instanceof CircuitOpenError) { /* circuit is open */ }
}
Import from the retry-pro/otel sub-path to avoid bundling OTEL into your main bundle.
import { withTracing } from 'retry-pro/otel';
const data = await withTracing(fetchDatabase, retryOptions, yourOtelTracer);
Emits span events on each retry attempt and sets the span status to OK or ERROR on completion.
import { retry, CircuitBreaker, RetryBudget } from 'retry-pro';
const circuit = new CircuitBreaker({ failureThreshold: 5, resetTimeout: 30000 });
const budget = new RetryBudget({ maxRetries: 50, window: 60000 });
try {
const data = await retry(async (signal) => axios.get('/api', { signal }), {
policy: 'httpSafe',
priority: 'high',
attemptTimeout: 5000,
adaptive: true,
hedging: { enabled: true, delay: 250 },
circuitBreaker: circuit,
retryBudget: budget,
onRetry: ({ attempt, strategyDelayMs }) => {
logger.warn(`Retry ${attempt}, waiting ${strategyDelayMs}ms`);
}
});
} catch (err) {
if (err instanceof CircuitOpenError) {
// Serve from cache or degrade gracefully
}
}
⚠️ Only retry idempotent operations. If a
POSTrequest creates a record but times out before returning a response, retrying it will create a duplicate. Use retry only for safe operations (reads, or writes protected by idempotency keys).
MIT
FAQs
A production-grade, policy-driven TypeScript retry package.
We found that retry-pro demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
RubyGems and Bundler 4.0.13 introduced an opt-in cooldown feature that delays newly published gems during dependency resolution.

Security News
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.

Security News
Federal audit finds NIST lacked a plan to clear the NVD backlog, wasted funds on duplicate work, and delayed use of CISA data.