Skip to main content
Carrier sends custom HTTP headers with each webhook request to provide message metadata and support dynamic backoff strategies. Understanding these headers is essential for implementing robust webhook handlers.

Headers Sent by Carrier

All custom headers sent by Carrier use the X-Carrier- prefix.

X-Carrier-Receive-Count

X-Carrier-Receive-Count
string
The number of times this message has been received from SQS.This corresponds to the SQS ApproximateReceiveCount attribute and indicates how many times the particular message has been delivered.Use cases:
  • Implement exponential backoff strategies
  • Track retry attempts
  • Trigger alerts for problematic messages
  • Apply different processing logic based on retry count
Example values: 1, 3, 5

X-Carrier-First-Receive-Time

X-Carrier-First-Receive-Time
string
The timestamp (in seconds since Unix epoch) when this message was first received from SQS.This corresponds to the SQS ApproximateFirstReceiveTimestamp attribute.Use cases:
  • Calculate message age
  • Implement time-based backoff strategies
  • Track message latency
  • Expire stale messages
Example values: 1710004800, 1710091200

Content-Type

Content-Type
string
default:"application/json"
The MIME type of the message body.This header is always sent with webhook requests. The value can be:
  1. Dynamic: Set per-message using the Body.ContentType SQS message attribute
  2. Configured: Set globally via CARRIER_WEBHOOK_DEFAULT_CONTENT_TYPE environment variable
  3. Default: application/json if neither is specified
Example values: application/json, application/xml, text/plain

Headers Received by Carrier

Carrier processes specific headers from webhook responses to implement dynamic behavior.

Retry-After

Retry-After
string
Used with HTTP 429 (Too Many Requests) responses to specify when to retry the message.Format: Number of seconds to wait before retryWhen your webhook returns a 429 status with this header, Carrier will:
  1. Parse the Retry-After value (in seconds)
  2. Calculate a new SQS visibility timeout
  3. Update the message visibility to prevent redelivery until after the specified time
This enables distributed backoff without requiring complex client-side logic.Example values: 30, 60, 300

Example Webhook Requests

First Delivery

When a message is delivered for the first time:
POST /webhook HTTP/1.1
Host: worker:9000
Content-Type: application/json
X-Carrier-Receive-Count: 1
X-Carrier-First-Receive-Time: 1710004800
Content-Length: 123

{"event": "user.created", "userId": "12345"}

Retry with Custom Content Type

A message with a custom content type on its third attempt:
POST /v1/events HTTP/1.1
Host: api.example.com
Content-Type: application/xml
X-Carrier-Receive-Count: 3
X-Carrier-First-Receive-Time: 1710004800
Content-Length: 256

<event><type>order.created</type><orderId>67890</orderId></event>

Implementing Dynamic Backoff

Here’s how to implement a distributed exponential backoff strategy in your webhook handler:
func handleWebhook(w http.ResponseWriter, r *http.Request) {
    // Extract receive count from header
    receiveCount := r.Header.Get("X-Carrier-Receive-Count")
    count, _ := strconv.Atoi(receiveCount)
    
    // Process the message
    err := processMessage(r.Body)
    if err != nil {
        // Calculate exponential backoff: 2^count seconds
        backoffSeconds := int(math.Pow(2, float64(count)))
        
        // Return 429 with Retry-After header
        w.Header().Set("Retry-After", strconv.Itoa(backoffSeconds))
        w.WriteHeader(http.StatusTooManyRequests)
        return
    }
    
    w.WriteHeader(http.StatusOK)
}

Tracking Message Age

Calculate how long a message has been in the system:
func getMessageAge(r *http.Request) time.Duration {
    firstReceiveTimeStr := r.Header.Get("X-Carrier-First-Receive-Time")
    firstReceiveTime, _ := strconv.ParseInt(firstReceiveTimeStr, 10, 64)
    
    messageTime := time.Unix(firstReceiveTime, 0)
    return time.Since(messageTime)
}

// Example usage
age := getMessageAge(r)
if age > 1*time.Hour {
    log.Printf("Warning: Message is %v old", age)
}

Setting Dynamic Content Types

To send messages with custom content types, use SQS message attributes:
import boto3

sqs = boto3.client('sqs')

# Send message with custom content type
sqs.send_message(
    QueueUrl='https://sqs.us-west-2.amazonaws.com/123456789/carrier-queue',
    MessageBody='<xml><data>value</data></xml>',
    MessageAttributes={
        'Body.ContentType': {
            'StringValue': 'application/xml',
            'DataType': 'String'
        }
    }
)
The SQS message attribute name must be exactly Body.ContentType for Carrier to recognize it and set the Content-Type header dynamically.

Response Status Codes

Carrier expects specific HTTP status codes from webhook endpoints:
Status CodeBehaviorDescription
200 OKMessage deleted from SQSSuccessful processing
429 Too Many RequestsVisibility timeout updatedRetry with backoff (requires Retry-After header)
Other (4xx, 5xx)Message returns to queueFailed processing, message will retry based on SQS configuration
If you return 429 without a Retry-After header, Carrier will log an error and treat it as a failed transmission. The message will return to the queue after the standard visibility timeout.

Best Practices

  1. Always handle X-Carrier-Receive-Count: Use this to implement intelligent retry strategies and prevent infinite loops
  2. Return 429 with Retry-After for rate limiting: This ensures proper backoff without losing messages
  3. Monitor message age: Use X-Carrier-First-Receive-Time to detect and handle stale messages
  4. Set appropriate Content-Type: Use SQS message attributes to specify content types for non-JSON payloads
  5. Implement exponential backoff: Use receive count to calculate increasing delays for retry attempts
  6. Return 200 only on success: Only return HTTP 200 when the message has been successfully processed and can be safely deleted

Build docs developers (and LLMs) love