Carrier supports dynamic visibility timeouts, allowing your worker application to control when messages are re-delivered from SQS. This enables sophisticated distributed backoff strategies, particularly useful for:
Rate limiting and throttling
Exponential backoff for external API calls
Scheduled retries based on business logic
Handling temporary failures gracefully
Unlike traditional retry mechanisms that rely on SQS’s fixed visibility timeout, dynamic timeouts give your application fine-grained control over message re-delivery timing.
Carrier includes metadata headers with every webhook request to help you implement intelligent retry logic:
Header
Description
Source
X-Carrier-Receive-Count
Number of times this message has been received from SQS
ApproximateReceiveCount attribute
X-Carrier-First-Receive-Time
Unix timestamp (seconds) of the first time this message was received
ApproximateFirstReceiveTimestamp attribute
These headers are defined in transmitter/webhook/transmitter.go:15-22:
const ( // HeaderPrefix is the prefix used for all HTTP request headers sent by the Transmitter. HeaderPrefix = "X-Carrier-" // HeaderRetryAfter is the standard Retry-After header. HeaderRetryAfter = "Retry-After" // HeaderContentType is the standard Content-Type header. HeaderContentType = "Content-Type")
The headers are populated from SQS message attributes in receiver/sqs/sqs.go:135-155:
func (h *handler) generateAttributes(m *message) transmitter.TransmitAttributes { attributes := make(transmitter.TransmitAttributes) for k, v := range m.Attributes { switch k { case SQSAttributeApproxomiteReceiveCount: attributes[TransmitAttributeReceiveCount] = v case SQSAttributeApproxomiteFirstReceiveTimestamp: attributes[TransmitAttributeFirstReceiveTime] = v } } // ... additional attribute handling return attributes}
When Carrier receives a 429 response, it extracts the Retry-After header value and updates the SQS message visibility timeout (transmitter/webhook/transmitter.go:113-132):
switch res.StatusCode {case http.StatusOK: // transmit successful return nilcase http.StatusTooManyRequests: // return a retryable error with the retry-after header value retryAfter := res.Header.Get(HeaderRetryAfter) if retryAfter != "" { seconds, err := strconv.Atoi(retryAfter) if err != nil { // cannot retry if we cannot parse the Retry-After header return fmt.Errorf("%w: %w: %w", transmitter.ErrTransmitFailed, ErrStatusCode429, err) } return transmitter.NewTransmitRetryableError(ErrStatusCode429, time.Duration(seconds*int(time.Second))) } return fmt.Errorf("%w: %w: %w", transmitter.ErrTransmitFailed, ErrStatusCode429, ErrNoRetryAfterHeader)default: // return a non-retryable error return fmt.Errorf("%w: %w: %d", transmitter.ErrTransmitFailed, ErrNon200StatusCode, res.StatusCode)}
func webhookHandler(w http.ResponseWriter, r *http.Request) { // Parse message to get scheduled time var msg Message json.NewDecoder(r.Body).Decode(&msg) scheduledTime := msg.ProcessAt // Unix timestamp now := time.Now().Unix() if now < scheduledTime { // Not yet time to process delay := scheduledTime - now w.Header().Set("Retry-After", strconv.FormatInt(delay, 10)) w.WriteHeader(http.StatusTooManyRequests) return } // Process the message processMessage(msg) w.WriteHeader(http.StatusOK)}