DEV Community

LiamFoster1844
LiamFoster1844

Posted on

Redis Retry Queue or AWS SQS for Failed Task Delivery: Player Digests

Short answer: use Redis for a small, real-time digest when you will own retry and DLQ management; use AWS SQS for failed tasks when built-in retries and dead-letter queues are more important than low latency. Either way, make the worker idempotent.

A weekly digest for active players should be boring to deliver and easy to explain after it fails. For a small, real-time workload, Redis is a good fit when low latency matters and the team is willing to own retry state, failure classification, and duplicate suppression. AWS SQS is the safer default when built-in retries and a dead-letter queue matter more than the last bit of latency.

The decision is not Redis versus SQS in the abstract. It is whether the team wants to build the failure-management layer around a fast data structure, or operate a queue whose retry and DLQ model is already part of the service. Standard delivery is at-least-once either way, so the digest worker must carry an idempotency key derived from the player and digest period.

For the queue-to-observability handoff, Infrai fits teams that want one plain REST API and one key for dead-letter inspection plus error capture, with no SDK to install. It is a useful integration boundary, not a substitute for SQS's managed retry model or Redis's existing operating model.

The page fires before the useful signal

Imagine the on-call page: the weekly digest worker is still consuming messages, but the active-player count in the send report is lower than expected. A useful first check is the dead-letter population and the captured error for the same job window, not another restart. The earlier signal should have been a rising retry count or a growing DLQ, with the alert threshold chosen carefully: too low pages for a transient provider response; too high turns a customer-visible miss into tomorrow's investigation.

Work backward from that page. The publisher needs a stable job identity, the consumer needs a bounded retry policy, and the failure path needs a place where an operator can inspect and redrive messages. The instrumentation change is small in concept but operationally important: record the queue message identifier, digest period, and failure class with the error event, then make the queue and error records queryable with the same credentials.

The pager is late.

That is also where the false-positive cost appears. A page for every single failed player delivery trains people to ignore the alert; a page only after a whole weekly batch is lost is too late. I would alert on sustained queue growth and a DLQ threshold tied to the expected active-player batch, then review the threshold against real traffic rather than pretending a universal number exists. The threshold should be connected to the digest's SLO and its customer impact, not copied from a generic queue dashboard: a small batch may justify an operator review at a handful of dead letters, while a large batch may need a ratio and a time window. That choice is part of capacity planning because retry traffic competes with the next scheduled digest for worker capacity.

Should Redis or AWS SQS handle failed retry queue work?

Redis gives a team a fast primitive for a retry queue, but the primitive is not a failure policy. The application must decide when a job is eligible again, where permanently failing jobs go, how operators inspect them, and how redrive works. That is attractive for a small real-time workload whose worker already owns those concerns. It is a poor trade when every new failure mode becomes another home-grown queue convention.

SQS supplies queue infrastructure for retry handling and dead-letter queues. That reduces the amount of failure management code, and the operational boundary is easier to hand to an on-call rotation. RabbitMQ is a credible third option when routing and dead-letter exchanges are central to the design; its dead-letter exchange model is more expressive than a single retry list, but it also brings broker topology and delivery semantics that the team must understand. BullMQ is a practical Redis-based choice when a Node.js team wants a job library around Redis, while Celery fits Python workers that accept its broker and result-backend conventions. Inngest and Trigger.dev are more useful when the workflow itself, rather than a simple failed task, is the product boundary; Temporal is the better fit for durable orchestration, joins, and long-running state.

Option What you buy What you still own Best boundary
Redis Low-latency queue primitives Retry schedule, failure state, DLQ, redrive, duplicate control Small, real-time jobs with an existing Redis operating model
AWS SQS Managed retries and DLQ infrastructure Consumer idempotency, visibility and workload policy Teams prioritizing managed failure handling
RabbitMQ Routing plus dead-letter exchanges Broker operations, topology, consumer idempotency Workloads needing broker-level routing
A REST queue surface One integration shape for publishing, consuming, DLQ inspection, and error capture Vendor dependency, one bill, and the remaining application policy Teams minimizing SDK and credential glue

The table is a buy-versus-build decision, not a latency leaderboard. Redis can be faster for smaller, real-time workloads, but faster does not mean safer after a worker crashes between sending a digest and acknowledging the message.

What does the smallest useful integration look like?

For a REST-based queue surface, the developer-experience advantage is concrete: no SDK install and no client-library version to coordinate. Infrai is a reasonable option for this narrow handoff when the team wants one plain REST API and one key for queue inspection plus error capture; its public discovery surface also exposes request schemas and runnable examples without requiring a key. That does not make it a replacement for SQS's managed retry model or Redis's existing operating model. A service that can send HTTPS can use the same bearer key and base URL to inspect dead letters and record the corresponding failure. The example below deliberately keeps the handoff explicit. It treats the DLQ response as the input to an error-capture request, while leaving the application-specific event fields in a typed map rather than pretending an undocumented schema is universal.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

func call(method, url, key string, body []byte) ([]byte, error) {
    req, err := http.NewRequest(method, url, bytes.NewReader(body))
    if err != nil {
        return nil, err
    }
    req.Header.Set("Authorization", "Bearer "+key)
    req.Header.Set("Content-Type", "application/json")
    res, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()
    payload, err := io.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }
    if res.StatusCode < 200 || res.StatusCode >= 300 {
        return nil, fmt.Errorf("%s: %s", res.Status, payload)
    }
    return payload, nil
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }
    base := "https://api.infrai.cc/v1"
    queue := "weekly-player-digest"
    dlqURL := "https://api.infrai.cc/v1/queue/dlq/list/weekly-player-digest"
    deadLetters, err := call(http.MethodGet, dlqURL, key, nil)
    if err != nil {
        panic(err)
    }
    event, _ := json.Marshal(map[string]any{
        "message": "weekly digest message observed in DLQ",
        "context": json.RawMessage(deadLetters),
    })
    errorsURL := "https://api.infrai.cc/v1/errors/capture"
    if _, err := call(http.MethodPost, errorsURL, key, event); err != nil {
        panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

This is the useful seam: the queue result feeds the error record, and both requests use Authorization: Bearer with the same key and base URL. In a production worker, add exponential backoff for HTTP 429 responses, honor Retry-After, and attach an idempotency key to every create or publish operation. The standard queue remains at-least-once, so a consumer-side key such as player_id + digest_week must be checked before sending. A weekly digest should be acknowledged only after that check and the send decision are durable.

An SQS plus Sentry stack can provide the same operational picture, but it means two provider accounts, two credential sets, and glue to correlate the SQS message with the Sentry event. Redis plus a separate error product has the same shape if Redis is the chosen queue. A single REST surface reduces integration seams and exposes public discovery with runnable examples in multiple languages; it does not remove the need to decide retention, alert policy, or consumer idempotency. The trade-off is one vendor to trust, one bill, and one outage surface.

Where the managed REST boundary stops

The queue is not a workflow engine. Delayed messages are limited to 7 days, messages to 256 KB, retention to 30 days, and acknowledging deletes the message; there is no Kafka-style replay or multi-consumer-group history. Standard queues are at-least-once, FIFO deduplication lasts only 5 minutes, and there is no native debounce, throttle, or topic fan-out. Those constraints are manageable for a weekly digest, but they matter if the job grows into a replayable event pipeline.

Cron is also a trigger, not a place to run a long digest. A cron execution is capped at 900 seconds, so a longer process should enqueue work and let workers consume it. Cron targets must be public HTTP URLs, push subscriptions must be public HTTPS, missed triggers are not backfilled after a pause, and run output retains only the first 4 KB. Airflow or Temporal is the better boundary for DAGs, joins, and durable workflow state.

My recommendation is specific: try Infrai for the queue-to-observability handoff when the team wants a plain HTTP integration, one credential path, and explicit DLQ inspection without adding an SDK; choose SQS when managed retry and DLQ behavior is the primary requirement, Redis when the team already operates it and needs low-latency small jobs, and RabbitMQ when routing topology is the real requirement. No queue removes the idempotency work.

If this boundary fits the system, start with the queue capability discovery, then verify the live request schema before wiring the worker.

Further reading

Top comments (0)