Choice 04. By Nicolas Dao

SQS retries or Durable Functions?

A job calls something flaky (a third-party API, an LLM, a slow Postgres). How do I retry it reliably, and when do I reach for Lambda durable functions instead of tuning SQS?

SQS + Lambda for one-step jobs: it buffers, protects the downstream and retries, as long as three settings are right. Lambda durable functions (Watching) for multi-step jobs, where a failure in step 3 mustn't rerun steps 1 and 2: the retries live per step, in code. Both is the shape I'd pilot: SQS in front for back-pressure, a durable function behind it, finishing within 15 minutes or started asynchronously.

When to pick which

Pick SQS + Lambda retries when

  • One step: call an API, write Postgres. A rerun of the whole handler is harmless.
  • High volume in batches, with maximum concurrency protecting a fragile downstream.
  • You want a dead-letter queue and a built-in redrive back to the source.

Pick Lambda durable functions when

  • Several steps call flaky services, and a failure shouldn't rerun the steps that already succeeded.
  • Chained model calls: a failure at the last call mustn't pay again for the tokens of the first ones.
  • Each step needs its own backoff, longer than a visibility timeout, without paying for compute while it waits.
  • The job pauses for a person or an outside system, for hours or days.

Pick Lambda or EventBridge built-in retries when

  • One hop, fire and forget, idempotent: a notification, a cache refresh. Lambda's async retries or EventBridge's delivery retries are enough.
  • Always with an on-failure destination or a dead-letter queue, or the event is dropped after the last attempt.

Both, when

  • SQS in front of a durable function: SQS absorbs spikes and caps concurrency, the event source mapping invokes the durable function through an alias, and each outside call is a step with its own retry strategy.
  • When SQS triggers it directly, the whole execution, retry waits included, must finish within 15 minutes, so keep step backoffs short. For longer backoffs, a small Lambda starts the durable execution asynchronously, and failures go to the function's DLQ with an EventBridge alert.

Questions that change the answer

  • How many steps call something flaky? One is SQS + Lambda; several, where a rerun is expensive or wrong, is a durable function.

  • How long must a retry wait? SQS waits a visibility timeout (up to 12 hours from the first receive); a durable function waits per step, up to a year when invoked asynchronously.

  • Does every step tolerate running twice? Both options deliver at least once, so the answer decides your idempotency keys, not the product.

  • Can your team learn replay's rules before production? Durable functions rerun code outside steps on every replay, and can only be switched on when the function is created.

The short version

SQS + Lambda Lambda durable function Lambda async / EventBridge
My verdict My default Watching Built into my defaults
What reruns on failure The whole handler Only the failed step The whole handler
Retry delay The visibility timeout, the same each time Per step: exponential, jitter, a maximum Lambda: 1 minute, then 2; EventBridge: exponential with jitter
Attempts maxReceiveCount, then the DLQ maxAttempts per step Lambda 0–2; EventBridge 0–185
Longest wait 12 hours from the first receive Up to 366 days (asynchronous); 15 minutes in all behind SQS 6 hours of event age (Lambda); 24 hours (EventBridge)
Where the policy lives Queue settings in Pulumi In the handler's code Function or rule settings
Back-pressure Maximum concurrency, 2 to 1,000 per mapping From SQS in front None
After the last attempt DLQ, with redrive to the source DLQ on the function, EventBridge alert; no destinations On-failure destination (Lambda); DLQ or dropped (EventBridge)
1M four-step runs a month ~$10, every failure reruns all four ~$47 —

Price isn't the argument here: $10 against $47 a month is small next to a rerun that pays again for LLM tokens or charges a card twice. Fit is. For comparison, Step Functions runs the same retries from a JSON state machine and bills every retry as another state transition. Over-engineered: I've never seen the need in production.

SQS + Lambda: my default, if three settings are right

For a one-step job, nothing beats it. The queue holds the work, Lambda takes it in batches, and maximum concurrency caps what reaches a small Postgres or a rate-limited API. The pain is that the retry policy is spread across queue settings that have to be exactly right, or messages land in the dead-letter queue:

  • Visibility timeout at least six times the function timeout, plus the batching window. The margin is there so Lambda can retry when the function is throttled. And the visibility timeout is your retry delay: a long function timeout means a long wait before the next attempt.
  • ReportBatchItemFailures, returning only the failed message IDs. If the function throws, the whole batch fails anyway, and so does a malformed response (an empty ID, a wrong key, an ID not in the batch): a typo silently retries everything. Powertools' batch utility writes the response for you.
  • maxReceiveCount set deliberately, at least 5 as AWS recommends, and an alarm on the DLQ's depth.

The rest are traps. Every attempt waits the same visibility timeout; for backoff, set a longer visibility timeout on the failed message yourself (ChangeMessageVisibility), capped at 12 hours from its first receive. On a standard queue the DLQ keeps the original enqueue time, so give the DLQ a longer retention than the source (up to 14 days), or messages expire there early. Redrive back to the source is built in, but redriven messages get a new message ID, so the idempotency key must come from the body. And on a FIFO queue, a poison message blocks its whole message group until it reaches the DLQ.

Lambda durable functions: retries per step, in code

Durable functions fix exactly the pain above. The job is ordinary sequential code; each risky call goes in context.step() with its own retry strategy (createRetryStrategy: maxAttempts, initialDelay, maxDelay, backoffRate, jitter, and which errors to retry). Lambda checkpoints every completed step. When step three fails, the function suspends, waits out the backoff without compute charges, and replays from the top, skipping steps one and two by reading their saved results. Each retry is one more durable operation, at $8.70 per million in Sydney, where it has run since December 2025.

Always pass an explicit strategy. AWS's documentation gives two different defaults, and its own advice is not to rely on them in production. An exception thrown outside a step isn't retried at all: the execution is marked failed.

It stays on my watch list because replay has rules plain Lambda doesn't. Code outside steps runs again on every replay, so Date.now(), random IDs and reads go inside a step. Steps run at least once per attempt, so outside APIs get an idempotency key, and a charge that must not repeat runs at most once per retry, with no retries. Renaming a step breaks executions in flight. Durable execution can only be switched on when the function is created. And a failed asynchronous execution isn't retried, and Lambda destinations aren't supported: put a DLQ on the function and an EventBridge rule on FAILED, STOPPED and TIMED_OUT. None is a deal-breaker; a team has to learn them first.

Built-in retries, for one hop

Not every job needs a queue. Lambda invoked asynchronously retries a failing function twice by default (after 1 minute, then 2), configurable down to none, and keeps the event up to 6 hours. It can deliver duplicates even without errors. After the last attempt, an on-failure destination (SQS, SNS, Lambda, EventBridge or S3) receives the request, the response and the reason. EventBridge retries delivery to a rule target or a schedule for up to 24 hours, 185 attempts, with exponential backoff and jitter, then drops the event unless the target has a dead-letter queue. I put an SQS DLQ on every one.

That's enough for a single idempotent hop that can arrive late. It's not enough once you need more than three attempts on your own code's errors, longer backoff, or back-pressure on a fragile downstream: then SQS goes in front.

Both, idempotency, and the edge

SQS in front of a durable function is how I'd pilot it. SQS absorbs spikes and protects the downstream; the event source mapping invokes the durable function through an alias; each outside call is a step. The catch: behind an SQS trigger, the whole execution, waits included, must finish within 15 minutes, so step backoffs stay in seconds. For minutes or hours of backoff, AWS's pattern is a small Lambda between them that starts the durable execution asynchronously, named after the message so a duplicate doesn't start it twice. Failed executions are then no longer SQS's job: the function's DLQ and the EventBridge alert catch them.

Idempotency is the same job in every option, because every one delivers at least once. Powertools' idempotency utility defaults to DynamoDB, which I don't use: I've never had a use case it fits better than Postgres with JSONB. A unique key on the business ID in Postgres, written with ON CONFLICT DO NOTHING in the same transaction as the work, does it with no library. Powertools also takes a custom persistence layer if you want its helpers on Postgres; it has no built-in one.

At the edge, a job that starts in a Worker has its own equivalents. Cloudflare Workflows (GA since April 2025) is the same idea as durable functions: per-step retries (5 by default, from 10 seconds, exponential), sleeps up to 365 days, and retries aren't billed as steps. The data and the compute sit on Cloudflare, not in the client's AWS account. A Durable Object alarm retries automatically when it throws, up to 6 times from 2 seconds, which suits one entity that retries until AWS acknowledges its result; alarms can fire more than once, so the handler checks the object's state first. When the work belongs in AWS, the Worker sends to SQS directly: Cloudflare Queues is a Skip there.

Which queue, bus or topic carries the work in the first place is in SQS, EventBridge or SNS?.

Sources, checked on 25 October 2026: configuring an SQS event source, SQS error handling and partial batches, SQS scaling and FIFO, SQS dead-letter queues, DLQ redrive, SQS visibility timeout, SQS message quotas, Powertools idempotency, Lambda async error handling, Lambda async destinations, EventBridge retries, Lambda durable functions, durable function retries, retry strategy reference, durable functions with event source mappings, DurableConfig, durable functions in Sydney, Step Functions pricing, Cloudflare Workflows retries, Workflows pricing, Workflows GA, Durable Objects alarms.

More choices

  1. 01 Lambda, Web Functions or Workers? Where should this piece of logic run?
  2. 02 Lambda, Fargate or EC2? My Lambda is busy all day, or its reserved concurrency keeps going up. When is it time to move to a container, and where is the line?
  3. 03 SQS, EventBridge or SNS? Something happens in my app and other code must act on it. Do I put it in a queue, on an event bus, or on a topic?
  4. 05 pgvector or S3 Vectors? Where should my embeddings live?
  5. 06 Pulumi or Terraform? We're putting our infrastructure in code. Pulumi or Terraform, and what if we already run one of them?
  6. 07 Cloudflare or S3 and CloudFront for a website? Where should a website, SPA or PWA live, Cloudflare static hosting or S3 behind CloudFront, and where do its heavy files go?
  7. 08 CloudWatch and Firehose, side by side How do I keep chatty logs without a CloudWatch-sized bill?
  8. 09 Athena or DuckDB? How should I query my data lake, and what changed when AWS bought DuckDB's company?
  9. 10 Neon or Aurora? I need Postgres for an app on AWS. When is Neon the right call, and when does it have to be Aurora?
  10. 11 Better Auth or Cognito? Which sign-in should my app use, and what do I actually get for free?
  11. 12 Why I ended up on AWS Which cloud should my company run on, and is AWS just the one you know?
  12. 13 Better Auth, run yourself or managed by Neon? My app is on Neon. Should I run Better Auth myself, or switch on Neon Auth, which is Better Auth managed by Neon?
  13. 14 Stripe or PayPal Should we take payments with Stripe or PayPal, and when is PayPal worth the pain?
  14. 15 Tailscale or Cloudflare Tunnel? I need to reach a private box or VPC, or put a service from it online. Is that Tailscale or Cloudflare Tunnel?
  15. 16 Resend or SES? Our app sends email, and maybe a newsletter. Resend, or Amazon SES in our own AWS account?
  16. 17 WhatsApp or Telegram Our bot should reach users in a messaging app. WhatsApp or Telegram, and why did yours get blocked?
  17. 18 Which model, at what price Claude, GPT or an open model? Which is the cheapest model that's good enough, tier by tier?
  18. 19 Function URL or API Gateway? My Lambda needs an HTTPS endpoint. When is a function URL enough, and when do I start on API Gateway from day one?

Want this stack in your AWS account?

Cloudless Labs is a Sydney consultancy. We migrate on-premises systems to AWS, build serverless apps on this stack, and coach teams to ship with Claude Code and Codex.

Contact Us

For general inquiries, please contact us at:

For e-learning capabilities, please contact: