Skip to main content
npayload is launching soon.
npayloadDocs
Guides

Error handling

HTTP error codes, SDK error types, rate limiting, idempotency, and debugging patterns

This guide covers how to handle errors from the npayload API and SDK, implement safe retries, and debug delivery failures.

HTTP error codes

The npayload API uses standard HTTP status codes.

CodeMeaningRetryable
200SuccessN/A
201CreatedN/A
400Bad request (invalid parameters)No
401Unauthorized (missing or invalid token)No (re-authenticate first)
403Forbidden (insufficient scopes)No
404Resource not foundNo
409Conflict (duplicate idempotency key with different payload)No
422Validation error (schema mismatch)No
429Rate limitedYes (after backoff)
500Internal server errorYes
502Bad gatewayYes
503Service unavailableYes

All error responses follow a consistent format:

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Too many requests. Retry after 2 seconds.",
    "retryAfter": 2
  }
}

SDK error types

The SDK throws typed errors that you can catch and handle specifically.

import {
  NPayloadClient,
  NPayloadError,
  NPayloadTimeoutError,
  NPayloadRateLimitError,
  NPayloadValidationError,
} from '@npayload/node';

try {
  await npayload.messages.publish('orders', {
    payload: { event: 'order.created' },
  });
} catch (error) {
  if (error instanceof NPayloadRateLimitError) {
    console.log(`Rate limited. Retry after ${error.retryAfter} seconds`);
  } else if (error instanceof NPayloadValidationError) {
    console.log('Validation failed:', error.details);
  } else if (error instanceof NPayloadTimeoutError) {
    console.log('Request timed out');
  } else if (error instanceof NPayloadError) {
    console.log(`API error ${error.statusCode}: ${error.message}`);
  }
}
Error classWhen thrown
NPayloadErrorBase class for all API errors
NPayloadTimeoutErrorRequest exceeded the configured timeout
NPayloadRateLimitErrorAPI returned 429. Includes retryAfter in seconds
NPayloadValidationErrorAPI returned 400 or 422. Includes details with field-level errors
NPayloadAuthErrorAPI returned 401 or 403. Token is invalid, expired, or lacks required scopes

Rate limiting

npayload enforces rate limits per organisation to protect the platform and ensure fair usage.

Rate limit headers

Every response includes rate limit headers:

HeaderDescription
X-RateLimit-LimitMaximum requests per window
X-RateLimit-RemainingRemaining requests in the current window
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds to wait before retrying (only on 429 responses)

Handling rate limits

The SDK provides built-in retry with backoff for rate limit errors.

const npayload = new NPayloadClient({
  auth,
  retries: {
    maxRetries: 3,
    retryOnRateLimit: true, // Automatically wait and retry on 429
  },
});

For manual handling:

try {
  await npayload.messages.publish('orders', { payload: data });
} catch (error) {
  if (error instanceof NPayloadRateLimitError) {
    await sleep(error.retryAfter * 1000);
    await npayload.messages.publish('orders', { payload: data });
  }
}

Idempotency for safe retries

Network errors and timeouts can leave you unsure whether a request succeeded. Use idempotency keys to make retries safe.

const idempotencyKey = `order-${orderId}-${Date.now()}`;

async function publishWithRetry(payload: any, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await npayload.messages.publish('orders', {
        payload,
        idempotencyKey,
      });
    } catch (error) {
      if (error instanceof NPayloadTimeoutError && attempt < maxRetries) {
        continue; // Safe to retry with the same idempotency key
      }
      if (error instanceof NPayloadRateLimitError) {
        await sleep(error.retryAfter * 1000);
        continue;
      }
      throw error; // Non-retryable error
    }
  }
}

Idempotency keys are valid for 24 hours. If you retry with the same key and the original request succeeded, npayload returns the original response without creating a duplicate message.

Circuit breaker patterns

When calling npayload from a critical path, implement a client-side circuit breaker to fail fast during outages.

class CircuitBreaker {
  private failures = 0;
  private lastFailure = 0;
  private readonly threshold = 5;
  private readonly cooldown = 30000; // 30 seconds

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.isOpen()) {
      throw new Error('Circuit breaker is open');
    }

    try {
      const result = await fn();
      this.failures = 0;
      return result;
    } catch (error) {
      this.failures++;
      this.lastFailure = Date.now();
      throw error;
    }
  }

  private isOpen(): boolean {
    if (this.failures < this.threshold) return false;
    return Date.now() - this.lastFailure < this.cooldown;
  }
}

const breaker = new CircuitBreaker();

await breaker.execute(() =>
  npayload.messages.publish('orders', { payload: data })
);

Debugging delivery failures

When messages are not reaching your consumers, check these areas in order.

1. Check the subscription status

const sub = await npayload.subscriptions.get('sub_abc123');
console.log(sub.status);        // "active" | "paused"
console.log(sub.circuitState);  // "closed" | "open" | "half-open"

If the circuit is open, your endpoint has been failing. Fix the endpoint and the circuit will reset.

2. Check the dead letter queue

const stats = await npayload.dlq.getStats({
  subscriptionGid: 'sub_abc123',
});

if (stats.totalEntries > 0) {
  const entries = await npayload.dlq.list({
    subscriptionGid: 'sub_abc123',
    limit: 5,
  });

  for (const entry of entries.items) {
    console.log(entry.failureReason);
  }
}

3. Verify routing key filters

If your subscription uses a routing key filter, make sure published messages match the pattern.

// Subscription filter: "order.created.*"
// This message matches:
await npayload.messages.publish('events', {
  payload: data,
  routingKey: 'order.created.us-east',
});

// This message does NOT match:
await npayload.messages.publish('events', {
  payload: data,
  routingKey: 'order.updated.us-east',
});

4. Check message delivery status

const delivery = await npayload.messages.getDeliveryStatus('msg_abc123');
for (const attempt of delivery.attempts) {
  console.log(attempt.subscriptionGid);
  console.log(attempt.status);     // "delivered" | "failed" | "pending"
  console.log(attempt.statusCode); // HTTP status from your endpoint
  console.log(attempt.attemptedAt);
}

Configuring timeouts

Set request timeouts to prevent your application from hanging on slow responses.

const npayload = new NPayloadClient({
  auth,
  timeout: 10000, // 10 seconds
});

For individual requests:

await npayload.messages.publish('orders', {
  payload: data,
}, { timeout: 5000 });

Best practices

  • Always catch NPayloadRateLimitError and respect the retryAfter value. Aggressive retrying only extends the rate limit window
  • Use idempotency keys for all publish operations in critical paths
  • Implement client-side circuit breakers to fail fast when npayload is experiencing issues
  • Log the full error object (including requestId from NPayloadError) for debugging. Include the request ID when contacting support
  • Set reasonable timeouts. The default is 30 seconds, but most operations complete in under 1 second
  • Handle NPayloadAuthError by refreshing your token (or let the SDK handle it automatically)
  • Monitor DLQ depth and delivery status as leading indicators of integration health

Next steps

Was this page helpful?

On this page