Skip to content

@j0nathan-ll0yd/resilience ​

Circuit breaker, retry, fetch timeout, and idempotency patterns for resilient Lambda functions.

CircuitBreaker ​

State machine-based circuit breaker with pluggable state persistence (in-memory or DynamoDB).

typescript
import { CircuitBreaker } from '@j0nathan-ll0yd/resilience'

const breaker = new CircuitBreaker({
  name: 'external-api',
  failureThreshold: 5,
  successThreshold: 2,
  resetTimeoutMs: 60000,
  stateStore: 'dynamodb',
  tableName: 'circuit-breaker-state',
})

const result = await breaker.execute(async () => {
  return await callExternalAPI()
})

CircuitBreakerConfig ​

typescript
interface CircuitBreakerConfig {
  name: string                        // Unique circuit name
  failureThreshold: number            // Failures before OPEN (default: 5)
  successThreshold: number            // Successes in HALF_OPEN before CLOSED (default: 2)
  resetTimeoutMs: number              // Ms before OPEN transitions to HALF_OPEN (default: 60000)
  stateStore: 'memory' | 'dynamodb'   // State backend (default: 'memory')
  tableName?: string                  // DynamoDB table (required when stateStore is 'dynamodb')
}

States ​

StateDescription
CLOSEDNormal operation, requests pass through
OPENFailure threshold exceeded, requests rejected immediately
HALF_OPENTesting recovery, limited requests allowed

Methods ​

MethodReturnsDescription
execute(operation)Promise<T>Run operation through the circuit breaker
getState()Promise<CircuitState>Current state without executing an operation
getFailureCount()Promise<number>Current cumulative failure count
reset()Promise<void>Reset to CLOSED with zero counts

CircuitBreakerOpenError ​

Thrown when calling execute() while the circuit is open and the reset timeout has not elapsed.

typescript
import { CircuitBreakerOpenError } from '@j0nathan-ll0yd/resilience'

try {
  await breaker.execute(() => callAPI())
} catch (error) {
  if (error instanceof CircuitBreakerOpenError) {
    // error.circuitName — name of the circuit
    // error.retryAfterMs — ms remaining before HALF_OPEN
  }
}

Retry ​

retryWithBackoff(operation, config?) ​

Execute an async function with exponential backoff retry. Uses the "full jitter" strategy recommended by AWS.

typescript
import { retryWithBackoff } from '@j0nathan-ll0yd/resilience'

const result = await retryWithBackoff(
  async () => await fetchData(),
  {
    maxRetries: 3,
    baseDelayMs: 1000,
    maxDelayMs: 5000,
    jitter: 0.5,
    isRetryable: (error) => error instanceof TransientError,
  },
)

RetryConfig ​

typescript
interface RetryConfig {
  maxRetries: number             // Max retry attempts (default: 3)
  baseDelayMs: number            // Base delay in ms (default: 1000)
  maxDelayMs: number             // Max delay cap in ms (default: 30000)
  jitter: number                 // Jitter factor 0-1; 0 = none, 1 = full (default: 1)
  isRetryable?: (error: unknown) => boolean  // Predicate for retryable errors
}

calculateDelayWithJitter(attempt, baseDelayMs, maxDelayMs, jitter) ​

Calculate delay with exponential backoff and jitter.

typescript
import { calculateDelayWithJitter } from '@j0nathan-ll0yd/resilience'

const delay = calculateDelayWithJitter(2, 1000, 30000, 1)

sleep(ms) ​

Promise-based sleep utility.

typescript
import { sleep } from '@j0nathan-ll0yd/resilience'

await sleep(1000)

fetchWithTimeout(url, options?) ​

Fetch with an automatic timeout using AbortSignal.timeout(). If the caller provides their own signal, the timeout and caller signal are combined via AbortSignal.any().

typescript
import { fetchWithTimeout } from '@j0nathan-ll0yd/resilience'

const response = await fetchWithTimeout('https://api.example.com/data', {
  timeoutMs: 5000,
  method: 'POST',
  body: JSON.stringify({ key: 'value' }),
  headers: { 'Content-Type': 'application/json' },
})

FetchWithTimeoutOptions ​

Extends RequestInit with:

OptionTypeDefaultDescription
timeoutMsnumber15000Timeout in milliseconds

Throws DOMException with name 'TimeoutError' when the timeout elapses.

Every call emits a debug log line carrying the request URL. Query-parameter values are redacted before that line is written — see redactUrlForLogging below. Credentials travel in query strings more often than callers intend, and the log sink outlives the request.

redactUrlForLogging(url) ​

Renders a URL for a log line with every query-parameter value replaced by REDACTED, keeping the parameter names, and with the fragment removed. fetchWithTimeout applies it automatically; it is exported so callers logging their own URLs can apply the same rule.

typescript
import { redactUrlForLogging } from '@j0nathan-ll0yd/resilience'

redactUrlForLogging('https://www.googleapis.com/books/v1/volumes?q=isbn:978...&key=AIzaSy...')
// 'https://www.googleapis.com/books/v1/volumes?q=REDACTED&key=REDACTED'

Parameter names are kept deliberately: they carry the diagnostic value (which endpoint was called, which arguments were supplied) while the values are what must not escape. A string that does not parse as a URL is truncated at the first ?, which is the safe direction for a function that must never leak.

Idempotency ​

Lambda idempotency powered by AWS Lambda Powertools. Ensures that retried or duplicate invocations produce the same result.

createIdempotencyStore(tableName?) ​

Create a DynamoDB-backed persistence store for idempotency state.

typescript
import { createIdempotencyStore } from '@j0nathan-ll0yd/resilience'

const store = createIdempotencyStore('my-idempotency-table')
// Falls back to IDEMPOTENCY_TABLE_NAME env var when tableName is omitted

createIdempotencyConfig(options?) ​

Create an idempotency configuration with optional TTL.

typescript
import { createIdempotencyConfig } from '@j0nathan-ll0yd/resilience'

const config = createIdempotencyConfig({ expiresAfterSeconds: 7200 })
// Default: 3600 seconds (1 hour)

makeIdempotent ​

Re-exported from @aws-lambda-powertools/idempotency. Wraps a handler or function to make it idempotent.

typescript
import { createIdempotencyStore, createIdempotencyConfig, makeIdempotent } from '@j0nathan-ll0yd/resilience'

const store = createIdempotencyStore()
const config = createIdempotencyConfig()

export const handler = makeIdempotent(async (event) => {
  // This handler returns the cached result for duplicate invocations
  return await processEvent(event)
}, {
  persistenceStore: store,
  config,
})

Additional re-exports ​

ExportSourceDescription
DynamoDBPersistenceLayer@aws-lambda-powertools/idempotency/dynamodbDynamoDB persistence adapter
IdempotencyConfig@aws-lambda-powertools/idempotencyConfiguration class

Timeouts ​

createTimeoutSignal(context, bufferMs?) ​

Create an AbortSignal that fires shortly before the Lambda itself times out, so an outbound call fails with a catchable abort instead of the invocation being killed mid-flight.

typescript
import { createTimeoutSignal } from "@j0nathan-ll0yd/resilience";

const signal = createTimeoutSignal(context, 3000);
const response = await fetch(url, { signal });

bufferMs defaults to 3000: the signal fires when context.getRemainingTimeInMillis() drops below that, leaving room to log the failure and return a response.