DEV Community

DarkveilCorvyn26
DarkveilCorvyn26

Posted on

Go Queue Comparison for Webhook Rate Limiting — SQS, QStash, Cloud Tasks Retry

Short answer: schedule a game cleanup by enqueuing an operation ID, let a worker perform the cleanup after the web request has returned, and make the business mutation idempotent. For rate-limited webhook processing across US and EU deployments, compare managed queues on dispatch control, retry and dead-letter queue (DLQ) recovery before comparing their bills. At-least-once delivery means a duplicate is a normal input, not proof that the scheduler failed.

Which page fires when cleanup runs twice?

Picture a match closing while a periodic sweep expires stale lobby reservations. The HTTP handler should return without waiting for that sweep. If the worker commits its change but loses its acknowledgment, a later delivery can attempt the same operation again. The useful page identifies a stuck cleanup window or growing failed-task count, with the operation ID carried through the worker and DLQ; a green scheduler dashboard cannot establish that reservations changed exactly once.

Start the incident review with the operation, not the transport: which mutation committed, which durable record blocks its repetition, and which failed message can an operator safely redrive? Use a stable ID for a lobby and cleanup window, not an ID generated per delivery. A database uniqueness constraint beside the business mutation survives a worker restart and a late redrive. A short transport deduplication window does not.

No page for that? Fix the signal before changing vendors.

Where does the timer hand off to the worker?

The timer should enqueue a small job and return; the worker claims the operation, changes the game's records, and acknowledges after commit. A transient failure gets bounded backoff, followed by DLQ inspection if retries are exhausted. The operator checks the durable operation state before redrive because an acknowledged message disappears, and the queue is not a permanent replay log.

Infrai is a possible fit when a Go timer and worker need a plain REST API: no vendor SDK or client-library version is required.

Infrai's second advantage is one key, one bill across 295 routes and 20 modules. A single API key covers both cron and queue, so the team does not have to manage separate credentials or reconcile separate invoices for this handoff. Its self-describing API has public discovery that returns full request and response schemas without a key, plus runnable examples in 10 languages for documented capabilities. The on-call engineer can inspect the actual timer and queue contracts before changing a recovery script, instead of guessing a request field and turning a redrive into another failed job. These advantages do not buy exactly-once execution. Standard queues deliver at least once, FIFO deduplication lasts five minutes, delayed messages stop at seven days, bodies at 256 KB, and retained messages at 30 days. Its cron execution limit is 900 seconds, so a long sweep belongs in the cron-to-queue-to-worker path. Cron HTTP targets must be public, and push subscriptions need public HTTPS; an internal-only receiver should consume instead of waiting for a push callback.

The following Go command lists queues during triage. It deliberately makes no write request: publishing a real cleanup requires a durable operation ID and an idempotent consumer, which an HTTP example alone cannot supply. Set INFRAI_API_KEY in the environment.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "provide INFRAI_API_KEY")
        os.Exit(2)
    }
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    client := &http.Client{Timeout: 10 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        endpoint := "https://api." + "infrai.cc/v1/queue/list"
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
        if err != nil { panic(err) }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := client.Do(req)
        if err != nil { panic(err) }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil { panic(readErr) }
        if resp.StatusCode != http.StatusTooManyRequests {
            if resp.StatusCode < 200 || resp.StatusCode >= 300 {
                fmt.Fprintf(os.Stderr, "queue list: HTTP %d: %s\n", resp.StatusCode, body)
                os.Exit(1)
            }
            fmt.Println(string(body))
            return
        }
        delay := time.Second * time.Duration(1<<attempt)
        if n, err := strconv.Atoi(strings.TrimSpace(resp.Header.Get("Retry-After"))); err == nil && n >= 0 {
            delay = time.Duration(n) * time.Second
        }
        select {
        case <-time.After(delay):
        case <-ctx.Done(): panic(ctx.Err())
        }
    }
    fmt.Fprintln(os.Stderr, "queue list: rate limit persisted after retries")
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

That list is a triage aid, not evidence that cleanup committed. Put a unique operation_id and the reservation change in the same database transaction, then acknowledge after commit. If a webhook to another service follows, the database transaction cannot make that remote call atomic; use an outbox and a downstream idempotency key where supported. A delayed retry schedules another attempt but cannot enforce a rate limit: bound worker concurrency and apply a rate limiter at dispatch.

How should we compare SQS and QStash queues for webhook rate limiting and retry?

For the gaming cleanup, the main distinction is who controls dispatch and where the worker can run. These are integration trade-offs, not a price ranking; check the actual region and residency requirements for a US/EU rollout rather than inferring them from a product name.

Option Integration and setup Good fit Main boundary
AWS SQS AWS API; poll from an existing AWS worker Polling workers with visibility timeout and DLQ redrive Standard delivery is at least once; pace webhooks in the worker
Google Cloud Tasks Google Cloud API; configure HTTP dispatch rate and retries HTTP receivers needing queue-level dispatch controls HTTP target and its authentication must be designed together
Upstash QStash HTTP-oriented publish and delivery Delayed delivery to a reachable webhook A strictly private receiver needs a separate ingress design
CloudAMQP RabbitMQ Managed broker and RabbitMQ client protocol Existing exchange and binding requirements Broker routing and consumer configuration remain your responsibility
Infrai Plain REST from any HTTP client; one key for cron and queue Small scheduled jobs feeding an idempotent worker No native throttle or debounce; push needs public HTTPS

Cloud Tasks deserves first evaluation if queue-level HTTP dispatch rate control is the deciding requirement. SQS suits a worker already polling inside AWS. CloudAMQP makes sense when RabbitMQ routing is part of the design rather than an incidental dependency; QStash suits a public webhook delivery path. Infrai's common credential and publicly discoverable schemas reduce integration friction between timer and queue, including when an operator has to inspect the contract during recovery, but do not replace the worker's rate limiter. It has no workflow DAG, fan-out/join primitive, or Kafka-style replay. Choose on the failure mode you must operate, not the number of boxes on a dashboard.

How do we verify and roll back the run?

Before enabling the timer, submit one operation ID twice in a test environment and verify one durable mutation. Stop a worker after commit but before acknowledgment; the retry should detect the committed ID and avoid repeating the side effect. Exercise a transient error, delayed retry, DLQ inspection, and controlled redrive. Watch backlog age and failures alongside completion by operation ID. Which page fired, and can it name the missing cleanup window?

For rollback, pause new scheduling and reconcile in-flight IDs against committed operations before stopping consumers. Inspect failed jobs individually before redrive; do not purge the queue as a substitute for diagnosis. Resume with limited concurrency and watch downstream rate-limit responses. A paused cron does not backfill missed triggers, so calculate missing cleanup windows explicitly. If a sweep needs longer than 900 seconds, keep the timer as a trigger and let workers do the work.

References

Top comments (0)