> ## Documentation Index
> Fetch the complete documentation index at: https://buttercms.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Retry logic & error handling

> ButterCMS webhooks use best-effort delivery without retries. Covers timeouts, error codes, idempotency strategies, and patterns for resilient endpoints.

## Delivery guarantees

* **Best-effort delivery**: ButterCMS attempts to deliver each webhook once; failed deliveries are logged but not automatically retried
* **Timeout**: Webhook endpoints must respond within 5 seconds
* **Status codes**: Return 2xx status codes to acknowledge successful processing

<Warning>
  ButterCMS does not automatically retry failed webhook deliveries. If your integration requires guaranteed delivery, implement your own retry logic or use a webhook proxy service.
</Warning>

## What counts as successful delivery?

A webhook is considered successfully delivered when:

* Connection is established to your endpoint
* Response is received within 5 seconds
* Status code is in the 2xx range (200-299)

```text theme={null}
✓ 200 OK
✓ 201 Created
✓ 202 Accepted
✓ 204 No Content
✗ 400 Bad Request
✗ 401 Unauthorized
✗ 500 Internal Server Error
✗ 503 Service Unavailable
```

### What causes delivery failure?

A webhook delivery fails if:

* **Connection failed** - Unable to establish TCP connection
* **Timeout** - No response received within 5 seconds
* **4xx/5xx response** - Server returned an error status code
* **Invalid SSL certificate** - HTTPS certificate validation failed

<Info>
  Failed webhook deliveries are logged on ButterCMS's side but are not automatically retried. Design your webhook handler to be resilient and consider implementing your own retry mechanism for critical integrations.
</Info>

## Timeout handling

Your webhook endpoint must respond within **5 seconds** or ButterCMS will consider the delivery failed.

### Why timeouts happen

* Synchronous processing of long-running tasks
* Database queries taking too long
* Calling external APIs without timeout limits
* Resource contention under high load

### Avoiding timeouts

**Use async processing** - Acknowledge the webhook immediately, then process asynchronously:

```javascript theme={null}
const Queue = require('bull');
const webhookQueue = new Queue('webhooks');

app.post('/webhooks/buttercms', async (req, res) => {
  // Immediately acknowledge receipt
  res.status(200).json({ received: true });

  // Queue the actual processing for later
  await webhookQueue.add('process-webhook', {
    payload: req.body,
    receivedAt: Date.now()
  });
});

// Process webhooks in the background
webhookQueue.process('process-webhook', async (job) => {
  const { payload } = job.data;

  // Do the actual work here - no timeout concerns
  await invalidateCache(payload.data.id);
  await updateSearchIndex(payload.data);
  await sendNotifications(payload);
});
```

### Timeout-safe patterns

| Pattern                | Description                             |
| ---------------------- | --------------------------------------- |
| **Queue-based**        | Add to job queue, process later         |
| **Event-driven**       | Emit internal event, return immediately |
| **Fire-and-forget**    | Start async operation, don't await      |
| **Background workers** | Separate process handles heavy lifting  |

## Proper error responses

Return appropriate status codes so ButterCMS knows how to handle failures:

### Return 2xx for success

Always return a 2xx status code when you've successfully received and queued the webhook:

```javascript theme={null}
// Good - acknowledges receipt
res.status(200).json({ received: true });
res.status(202).json({ queued: true });
res.status(204).send(); // No content needed
```

### Return 4xx for client errors

Return 4xx for permanent client-side errors:

```javascript theme={null}
// Invalid authentication
if (!isValidSecret(req.headers['x-webhook-secret'])) {
  return res.status(401).json({ error: 'Unauthorized' });
}

// Invalid payload
if (!isValidPayload(req.body)) {
  return res.status(400).json({ error: 'Invalid payload' });
}
```

### Return 5xx for server errors

Return 5xx for server-side errors:

```javascript theme={null}
// Temporary failure
if (!databaseConnection) {
  return res.status(503).json({ error: 'Service temporarily unavailable' });
}

// Unexpected error
catch (error) {
  console.error('Webhook processing error:', error);
  return res.status(500).json({ error: 'Internal server error' });
}
```

### Response code decision guide

```mermaid theme={null}
flowchart TD
    A[Incoming Request] --> B{Is the request valid?}
    B -->|No| C[400 Bad Request]
    B -->|Yes| D{Is authentication valid?}
    D -->|No| E[401 Unauthorized]
    D -->|Yes| F{Is your service healthy?}
    F -->|No| G[503 Service Unavailable]
    F -->|Yes| H{Did processing succeed?}
    H -->|Yes| I[200 OK]
    H -->|No, temporary issue| J[500 Internal Server Error]
    H -->|No, permanent issue| K[422 Unprocessable Entity]
```

## Handling duplicate deliveries

While ButterCMS uses best-effort delivery, your endpoint may occasionally receive duplicate webhooks due to network conditions. Implementing idempotency is a best practice for any webhook handler:

### Why duplicates may occur

* Network issues during response transmission
* Your server processed successfully but response was lost
* Infrastructure failover scenarios

### Idempotency strategies

**1. Track by Unique Identifier**

Create a unique ID from the webhook data:

```javascript theme={null}
function getWebhookId(payload) {
  const { data, webhook } = payload;
  // Combine event + content ID + timestamp for uniqueness
  return `${webhook.event}:${data.id}:${data.timestamp}`;
}
```

**2. Check Before Processing**

```javascript theme={null}
const processed = new Set(); // Use Redis in production

async function handleWebhook(payload) {
  const id = getWebhookId(payload);

  if (processed.has(id)) {
    console.log(`Duplicate webhook ignored: ${id}`);
    return { status: 'duplicate' };
  }

  processed.add(id);

  try {
    await processWebhook(payload);
    return { status: 'processed' };
  } catch (error) {
    processed.delete(id); // Allow retry on error
    throw error;
  }
}
```

**3. Make operations idempotent**

Design your processing logic to produce the same result when run multiple times:

```javascript theme={null}
// Idempotent: Set value (same result if run twice)
await cache.set(`page:${data.id}`, null);

// NOT idempotent: Increment (different result each time)
await analytics.incrementPageViews(data.id);  // Don't do this!

// Fix: Use set with unique event ID
await analytics.recordEvent(`pageview:${data.id}:${data.timestamp}`);
```

## Graceful degradation

Build resilient webhook handlers that degrade gracefully when dependencies fail:

### Circuit breaker pattern

```javascript theme={null}
const CircuitBreaker = require('opossum');

const cacheInvalidation = new CircuitBreaker(invalidateCache, {
  timeout: 5000,
  errorThresholdPercentage: 50,
  resetTimeout: 30000
});

app.post('/webhooks/buttercms', async (req, res) => {
  try {
    // Primary action - cache invalidation
    await cacheInvalidation.fire(req.body.data.id);
  } catch (error) {
    // Circuit open or failed - log but don't fail the webhook
    console.error('Cache invalidation failed, will retry manually:', error);
  }

  // Always acknowledge receipt
  res.status(200).json({ received: true });
});
```

### Fallback processing

```javascript theme={null}
async function processWebhook(payload) {
  try {
    // Try primary processing
    await primaryHandler(payload);
  } catch (primaryError) {
    console.warn('Primary handler failed:', primaryError);

    try {
      // Try fallback
      await fallbackHandler(payload);
    } catch (fallbackError) {
      // Queue for manual review
      await deadLetterQueue.add(payload);
      console.error('Webhook queued for manual processing');
    }
  }
}
```

## Monitoring failed deliveries

Track webhook failures to identify issues early:

### Logging best practices

```javascript theme={null}
app.post('/webhooks/buttercms', async (req, res) => {
  const startTime = Date.now();
  const { data, webhook } = req.body;

  console.log('Webhook received', {
    event: webhook.event,
    contentId: data.id,
    timestamp: data.timestamp
  });

  try {
    await processWebhook(req.body);

    console.log('Webhook processed', {
      event: webhook.event,
      contentId: data.id,
      duration: Date.now() - startTime
    });

    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook failed', {
      event: webhook.event,
      contentId: data.id,
      error: error.message,
      duration: Date.now() - startTime
    });

    res.status(500).json({ error: 'Processing failed' });
  }
});
```

### Alerting on failures

Set up alerts for:

* Webhook endpoint downtime
* High error rates (>5% failures)
* Processing latency spikes
* Circuit breaker trips

## Best practices summary

| Practice             | Recommendation                                    |
| -------------------- | ------------------------------------------------- |
| **Response time**    | Return 200 within 5 seconds, process async        |
| **Status codes**     | Use 2xx for success, 4xx/5xx appropriately        |
| **Idempotency**      | Always handle duplicate deliveries                |
| **Error handling**   | Catch exceptions, log details, degrade gracefully |
| **Monitoring**       | Track success rates, latency, and failures        |
| **Timeouts**         | Set timeouts on all external calls                |
| **Queue processing** | Use background workers for heavy operations       |
