Skip to content

Retry Logic

FLUID automatically retries failed webhook deliveries to ensure reliable event notification. If your endpoint is temporarily unavailable or returns an error, FLUID will retry the webhook multiple times with exponential backoff.

How Retry Logic Works


Retry Schedule

FLUID uses exponential backoff with jitter for retry attempts:

AttemptDelayTotal Time ElapsedStatus
1 (initial)0s0sImmediate delivery
2~2s~2sFirst retry
3~4s~6sSecond retry
4~8s~14sThird retry
5~16s~30sFourth retry
6 (final)~32s~62sFinal retry

Default Configuration

  • Max Attempts: 6 (1 initial + 5 retries)
  • Max Delay: 300 seconds (5 minutes)
  • Timeout: 30 seconds per attempt
  • Connection Timeout: 10 seconds

These values can be customized per payment partner. Contact your integration manager for custom configurations.

Exponential Backoff Formula

base_delay = 2^(attempt_count)
jitter = random(0, base_delay * 0.1)
actual_delay = min(base_delay + jitter, 300)

Jitter (random delay) prevents thundering herd problem when multiple webhooks fail simultaneously.


When Webhooks Are Retried

FLUID automatically retries webhooks in these scenarios:

1. HTTP Status Codes (Retriable)

Status CodeDescriptionRetry?Reason
408Request Timeout✅ YesTemporary timeout, likely recoverable
429Too Many Requests✅ YesRate limited, retry after backoff
500Internal Server Error✅ YesTemporary server issue
502Bad Gateway✅ YesGateway/proxy issue
503Service Unavailable✅ YesTemporary unavailability
504Gateway Timeout✅ YesGateway timeout

2. Network Errors (Retriable)

Error TypeRetry?Reason
Connection timeout✅ YesServer didn't respond within 10s
Read timeout✅ YesResponse not received within 30s
Connection refused✅ YesServer not accepting connections
DNS resolution failure✅ YesTemporary DNS issue
SSL/TLS errors✅ YesCertificate or handshake issues

3. HTTP Status Codes (Non-Retriable)

Status CodeDescriptionRetry?Reason
200-299Success❌ NoWebhook delivered successfully
400Bad Request❌ NoInvalid request, won't succeed on retry
401Unauthorized❌ NoAuthentication issue (check signature verification)
403Forbidden❌ NoAccess denied
404Not Found❌ NoEndpoint doesn't exist
405Method Not Allowed❌ NoWrong HTTP method
410Gone❌ NoEndpoint permanently removed

Client Errors Are Not Retried

4xx errors (except 408 and 429) indicate client-side issues that won't be resolved by retrying. Fix your endpoint and contact support to resend failed webhooks.


Webhook Delivery States

Each webhook delivery has one of these states:

pending

Initial state when webhook is created and waiting for delivery or retry.

json
{
  "id": 12345,
  "status": "pending",
  "attempt_count": 0,
  "max_attempts": 5,
  "next_attempt_at": "2025-01-28T10:15:00Z"
}

delivered

Successfully delivered (received 2xx response).

json
{
  "id": 12345,
  "status": "delivered",
  "attempt_count": 1,
  "response_code": 200,
  "response_body": "{\"received\":true}",
  "delivered_at": "2025-01-28T10:15:05Z"
}

failed

Failed temporarily and eligible for retry.

json
{
  "id": 12345,
  "status": "failed",
  "attempt_count": 2,
  "max_attempts": 5,
  "response_code": 500,
  "response_body": "Internal Server Error",
  "next_attempt_at": "2025-01-28T10:15:10Z"
}

abandoned

Failed permanently after exhausting all retries.

json
{
  "id": 12345,
  "status": "abandoned",
  "attempt_count": 6,
  "max_attempts": 5,
  "response_code": 503,
  "response_body": "Service Unavailable",
  "failed_at": "2025-01-28T10:16:30Z"
}

Handling Retries in Your Endpoint

1. Respond Quickly

Always respond with a 2xx status code within 30 seconds:

javascript
app.post('/webhooks/fluid', async (req, res) => {
  // ✅ CORRECT: Acknowledge receipt immediately
  res.status(200).json({ received: true });
  
  // Process asynchronously (don't block response)
  processWebhookAsync(req.body);
});

Don't do this:

javascript
app.post('/webhooks/fluid', async (req, res) => {
  // ❌ WRONG: Blocking response with slow processing
  await slowDatabaseOperation();
  await sendEmail();
  await updateInventory();
  
  res.status(200).json({ received: true });
});

2. Implement Idempotency

Use event_id to prevent duplicate processing when webhooks are retried:

javascript
const processedEvents = new Map(); // Or use database

app.post('/webhooks/fluid', async (req, res) => {
  const eventId = req.body.event_id;
  
  // Check if already processed
  if (processedEvents.has(eventId)) {
    console.log(`Event ${eventId} already processed`);
    return res.status(200).json({ received: true, duplicate: true });
  }
  
  // Verify signature
  if (!verifySignature(req.rawBody, req.headers['x-fluid-signature'])) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Mark as processing
  processedEvents.set(eventId, { status: 'processing', timestamp: Date.now() });
  
  // Acknowledge receipt
  res.status(200).json({ received: true });
  
  // Process asynchronously
  try {
    await processWebhook(req.body);
    processedEvents.set(eventId, { status: 'completed', timestamp: Date.now() });
  } catch (error) {
    console.error(`Error processing webhook ${eventId}:`, error);
    processedEvents.set(eventId, { status: 'error', error: error.message });
  }
});

3. Handle Rate Limiting

If your system has rate limits, return 429 status to signal FLUID to retry:

javascript
const rateLimiter = new RateLimiter({ maxRequests: 100, windowMs: 60000 });

app.post('/webhooks/fluid', async (req, res) => {
  // Check rate limit
  if (!rateLimiter.allow()) {
    console.log('Rate limit exceeded, requesting retry');
    return res.status(429).json({ 
      error: 'Rate limit exceeded',
      retry_after: 60 
    });
  }
  
  // Process webhook
  // ...
});

4. Return Appropriate Status Codes

Guide FLUID's retry behavior with correct HTTP status codes:

javascript
app.post('/webhooks/fluid', async (req, res) => {
  try {
    // Verify signature
    if (!verifySignature(req.rawBody, req.headers['x-fluid-signature'])) {
      // Don't retry - signature will never be valid
      return res.status(401).json({ error: 'Invalid signature' });
    }
    
    // Check if endpoint is being deprecated
    if (isEndpointDeprecated()) {
      // Don't retry - endpoint is gone
      return res.status(410).json({ error: 'Endpoint deprecated' });
    }
    
    // Temporary database issue
    if (isDatabaseUnavailable()) {
      // Retry - database might recover
      return res.status(503).json({ error: 'Database temporarily unavailable' });
    }
    
    // Process successfully
    await processWebhook(req.body);
    return res.status(200).json({ received: true });
    
  } catch (error) {
    console.error('Webhook processing error:', error);
    // Retry - unknown error might be temporary
    return res.status(500).json({ error: 'Internal server error' });
  }
});

Monitoring Webhook Deliveries

Check Delivery Status

Contact your FLUID integration manager to access webhook delivery monitoring dashboard:

  • Delivery Success Rate: Percentage of webhooks delivered successfully
  • Average Attempts: Average number of attempts before success
  • Abandoned Webhooks: Webhooks that failed all retries
  • Response Time: Time taken for your endpoint to respond
  • Failure Reasons: Common error codes and messages

Example Metrics

Last 24 Hours:
- Total Webhooks:    1,234
- Delivered:         1,198 (97.1%)
- Abandoned:            12 (1.0%)
- Pending:              24 (1.9%)
- Average Attempts:   1.3
- P95 Response Time: 850ms

Common Failure Patterns

PatternCauseSolution
High timeout rateSlow endpoint processingImplement async processing
Abandoned during deploymentsServer downtimeUse rolling deployments
401 errorsWrong signature verificationVerify webhook secret
503 errorsResource exhaustionScale infrastructure
Connection refusedFirewall/network issueCheck firewall rules

Troubleshooting Failed Deliveries

Abandoned Webhooks

If webhooks are being abandoned (failed all retries):

1. Check Endpoint Health

bash
# Test endpoint accessibility
curl -X POST https://your-domain.com/webhooks/fluid \
  -H "Content-Type: application/json" \
  -d '{"test": true}'

# Expected: 200 OK (or 401 if signature verification fails)

2. Check Response Time

bash
# Measure response time
time curl -X POST https://your-domain.com/webhooks/fluid \
  -H "Content-Type: application/json" \
  -d '{"test": true}'

# Should complete within 30 seconds

3. Check Server Logs

Look for webhook processing errors:

bash
# Check application logs
tail -f /var/log/app.log | grep "webhooks/fluid"

# Check for errors
grep -i "error" /var/log/app.log | grep "webhook"

4. Verify Signature Implementation

javascript
// Add debug logging
function verifySignature(rawBody, signature, secret) {
  console.log('Raw body length:', rawBody.length);
  console.log('Signature:', signature);
  
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  
  console.log('Expected signature:', expectedSignature);
  console.log('Signatures match:', signature === expectedSignature);
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

Request Manual Resend

If webhooks are abandoned and you've fixed the issue, contact your FLUID integration manager to manually resend failed webhooks:

Provide:

  • Event ID(s) or date range
  • Transaction reference(s)
  • Reason for resend
  • Confirmation that endpoint is now healthy

Best Practices

✅ Do's

  1. Return 2xx Immediately: Acknowledge receipt before processing
  2. Process Asynchronously: Queue webhook for background processing
  3. Implement Idempotency: Use event_id to prevent duplicate processing
  4. Log Everything: Store webhook delivery IDs, timestamps, and outcomes
  5. Monitor Metrics: Track success rates, response times, and failures
  6. Handle Retries Gracefully: Recognize and handle duplicate webhooks
  7. Return Appropriate Status Codes: Guide retry behavior correctly

❌ Don'ts

  1. Don't Block Response: Never perform long operations before responding
  2. Don't Return Errors for Business Logic Failures: Return 2xx even if your logic fails
  3. Don't Ignore Idempotency: Always check for duplicate events
  4. Don't Hardcode Timeouts: Be ready for 30-second timeout
  5. Don't Skip Signature Verification: Never trust webhooks without verification

Advanced Configuration

Custom Retry Settings

Default retry settings work for most integrations, but custom configurations are available:

SettingDefaultRangeDescription
max_attempts51-10Maximum retry attempts
timeout30s10-60sRequest timeout per attempt
connection_timeout10s5-30sConnection establishment timeout

Contact your integration manager to customize these settings.

Retry for Specific Events Only

Configure selective retries based on event importance:

High Priority (max 10 retries):
- transaction.completed
- transaction.failed

Medium Priority (max 5 retries, default):
- transaction.processing
- transaction.reversed

Low Priority (max 2 retries):
- transaction.created
- transaction.pending

Testing Retry Behavior

Simulate Failures

Test retry logic by returning different status codes:

javascript
let attemptCount = 0;

app.post('/webhooks/fluid', (req, res) => {
  attemptCount++;
  
  // Simulate failures for first 3 attempts
  if (attemptCount <= 3) {
    console.log(`Attempt ${attemptCount}: Returning 503`);
    return res.status(503).json({ error: 'Service unavailable' });
  }
  
  // Succeed on 4th attempt
  console.log(`Attempt ${attemptCount}: Success`);
  res.status(200).json({ received: true });
});

Test Idempotency

javascript
const processedEvents = new Set();

app.post('/webhooks/fluid', (req, res) => {
  const eventId = req.body.event_id;
  
  if (processedEvents.has(eventId)) {
    console.log(`Duplicate event: ${eventId}`);
    return res.status(200).json({ received: true, duplicate: true });
  }
  
  processedEvents.add(eventId);
  console.log(`New event: ${eventId}`);
  
  res.status(200).json({ received: true, processed: true });
});

Next Steps

Webhooks Overview →

Complete introduction to FLUID webhooks and getting started guide.

Event Types →

Explore all available webhook events and their lifecycle.

Error Handling Guide →

Learn how to handle errors and failures in your integration.


See Also