Short answer: choose Standard semantics when throughput and recovery latency matter more than delivery order, and choose FIFO semantics when a retry must be serialized with related work. For a nightly payment-provider reconciliation, the decision is less about the queue name than about the idempotency key, the message group, and the alert that tells an operator the retry budget is being consumed.

The useful boundary is this: Standard delivery is at least once with best-effort ordering, so every handler must tolerate duplicates and reordering. FIFO provides ordered processing within a message group and deduplication behavior, but that ordering can turn one slow or poisoned job into a queue-wide delay for that group. A queue choice cannot repair a non-idempotent reconciliation write.

Here is the smallest useful worker boundary:

func process(runID, accountID string, payload []byte) error { key := runID + "/" + accountID return storeOnce(key, payload) } 

The page fires before the queue choice looks interesting

The page an on-call engineer sees is rarely “FIFO or Standard is wrong.” It is usually a growing age-of-oldest-message signal, a retry count approaching its limit, or a reconciliation window that has not closed. The immediate task is to find out whether the provider is slow, the worker is failing, or one logical account is blocking later messages.

For the nightly job, I want the alert to preserve three identifiers: the reconciliation run, the payment-provider account, and the attempt number. A generic queue depth alert is useful, but it cannot distinguish a broad provider slowdown from one repeatedly failing account. Those cases have different actions and different risk.

The first instrumentation change is structured outcome data around each delivery:

\`go
package main

import (
"log/slog"
"strconv"
)

func recordOutcome(logger *slog.Logger, runID, accountID string, attempt int, outcome string) {
logger.Info("reconciliation delivery",
"run_id", runID,
"account_id", accountID,
"attempt", strconv.Itoa(attempt),
"outcome", outcome,
)
}
`\

This gives the operator a path from alert to one logical job. It also makes a false-positive threshold visible. Alert too early and a normal provider pause wakes someone up; alert too late and the retry window can expire before anyone notices. I prefer a threshold tied to the reconciliation deadline and retry budget, because queue depth alone has no business meaning.

Should AWS SQS FIFO or Standard queues handle retries?

Consider two messages for the same account: close-2026-10-26\ and reconcile-2026-10-26\. If the close operation is delivered after the reconciliation operation, the result may be coherent. If the order is reversed, the worker might observe an account state that is valid for neither operation. Related messages can share a message group so the consumer processes that group in order.

The cost is serialization. A failed message that remains in the group can delay later messages in that same group. That is useful when later work depends on earlier work. It is an outage multiplier when messages are independent but were grouped by convenience.

The limitation is important: neither queue mode guarantees that a payment reconciliation is correct by itself. Standard is a poor fit when reordering changes a state transition and the handler cannot make writes idempotent. FIFO is a poor fit when thousands of independent jobs are placed in one group, because one slow delivery can delay unrelated work. The right choice depends on the dependency boundary, not on a universal winner.

Standard queues make a different promise. A worker can receive deliveries in an order that is not the order sent, and the same delivery can appear again. The handler should write the reconciliation result with a stable idempotency key, make the external operation safe to repeat, and acknowledge the message only after the durable state transition succeeds.

A good test is deliberately boring: deliver the same message twice, deliver messages 2 and 1 in that order, terminate the worker after the provider call but before acknowledgement, then replay the message. The final ledger state should be the same. If it is not, FIFO is hiding a correctness problem rather than solving it.

Choosing between latency and cost under failure

For a nightly reconciliation, latency includes provider response time, visibility timeout, backoff, redelivery, and the time needed to drain a backlog before the reconciliation deadline. Cost includes worker time and operational attention, not just queue requests.

Standard is a sensible default when thousands of independent account checks can proceed concurrently and duplicate work is cheap relative to delaying the whole run. It can reduce recovery time from a provider slowdown because healthy messages are not required to wait behind one logical group. The handler and downstream writes must be idempotent.

FIFO earns its complexity when the business rule is explicitly sequential: a series of mutations for one account, a state machine transition, or a retry stream where later work must not pass an earlier failure. The message group should represent that dependency, not the entire reconciliation run. One group for everything converts a single bad account into a global lock.

I write the decision as a rule before choosing the queue:

  • If reordering can change the business result, preserve order for that dependency and keep groups narrow.
  • If reordering cannot change the result, prefer concurrency and make duplicate delivery harmless.
  • If either answer is uncertain, test the handler with duplicates, delayed acknowledgements, and out-of-order input before treating queue semantics as a fix.

The point is to choose where the system spends its scarce resource: serialized progress, worker capacity, or operator attention.

A minimal retry worker with an idempotency boundary

The worker below keeps queue mechanics behind an interface. It does not assume that receiving a message means the payment-provider operation succeeded. The durable write is the boundary that decides whether a retry is safe.

\`go
package main

import (
"context"
"errors"
)

type Delivery struct {
RunID string
AccountID string
Attempt int
Body []byte
}

var ErrTemporary = errors.New("temporary provider failure")

func handle(ctx context.Context, d Delivery, provider func(context.Context, []byte) error, store func(context.Context, string, []byte) error) error {
if err := provider(ctx, d.Body); err != nil {
return err
}

key := d.RunID + "/" + d.AccountID return store(ctx, key, d.Body) 

}
`\

In production, store\ needs an atomic uniqueness or upsert rule for that key, and the provider call needs its own idempotency contract or a reconciliation-safe read-before-write design. The queue cannot provide those for you. A retryable error should return according to the retry policy; a permanent validation error should go to a dead-letter path with enough context for a later decision.

The retry policy also needs a deadline. A message that can retry forever is an unbounded source of work. Keep the original run deadline, attempt count, and last error in the message or its durable job record. On every retry, measure age and remaining budget. If the remaining budget is smaller than the expected provider call plus backoff, stop retrying and surface the job for review.

Where the earlier signal should come from

A useful dashboard has separate views for queue age, delivery attempts, provider latency, successful durable writes, and dead-letter volume. Slice those by account or message group. The alert should fire on a condition that predicts a missed reconciliation deadline, not merely on a transient spike in messages.

For FIFO, watch blocked groups and the age of the oldest message in each group. For Standard, watch duplicate processing and out-of-order state transitions in addition to age. In both modes, attach a run identifier so the operator can answer one question quickly: “Will this reconciliation finish correctly within its window?”

I initially treat a growing retry count as a worker problem. The correction is to inspect the distribution. A broad increase usually points to a dependency or provider issue; a narrow increase often points to bad input, an account-specific state transition, or a poison message. The queue metric is the symptom. The grouped, structured outcome is the diagnostic signal.

Do not page on every duplicate. Duplicates are expected under at-least-once delivery and can also arise around acknowledgement boundaries. Page when duplicates change durable state, consume the retry budget, or push the run past its deadline. That keeps the alert useful.

Further reading