DEV Community

Cover image for Serverless Cost Mistakes I've Seen (and How to Avoid Them)
Muhammad Awais Zahid
Muhammad Awais Zahid

Posted on Edited on

Serverless Cost Mistakes I've Seen (and How to Avoid Them)

Serverless Cost Mistakes I've Seen (and How to Avoid Them)

Oct 30, 2026 · @muhammad Awais

Serverless is cheap until it isn't

Serverless pricing rewards good design, and three habits quietly make bills several times larger than they need to be: over-provisioned memory, runaway retries, and chatty architectures.

You pay per use, which is exactly why small mistakes scale. A setting that costs a fraction of a cent per invocation becomes real money at millions of invocations a day.

This article covers each mistake, how to spot it, and the fix. The examples use AWS SAM and Python, but the ideas apply to any infrastructure tool or runtime.

Mistake 1: Guessing Lambda memory instead of measuring it

Most teams pick a memory size once, often 128 MB or 1024 MB, and never revisit it, but the right size is the one that minimizes cost, not the one that looks smallest. Lambda bills memory (in GB) multiplied by duration (in seconds), and the memory setting also controls how much CPU your function gets.

That creates a counter-intuitive result: for CPU-bound code, more memory can make a function finish so much faster that the invocation costs the same or less. The numbers below are illustrative, not from a real workload.

Memory Duration Cost per invocation (GB-seconds)
128 MB 1,200 ms 0.150
512 MB 280 ms 0.140
1024 MB 140 ms 0.140
2048 MB 100 ms 0.200

In this example, 1024 MB costs the same as 128 MB but responds roughly 8.5 times faster. Past that point, duration stops dropping and you only pay for memory you can't use.

The reverse also happens. Functions that mostly wait on a database or an API don't get faster with more CPU, so extra memory is pure waste. [Add a real before/after from your own account, for example: "I cut a function from X MB to Y MB and saved Z%."]

How to fix it

  • Measure, don't guess. Run the open-source AWS Lambda Power Tuning tool, which tests your function at several memory sizes and charts cost against speed. AWS Compute Optimizer can also recommend memory sizes from real usage.
  • Try arm64. Functions on the Graviton (arm64) architecture are billed at a lower rate per GB-second than x86, and many workloads run as fast or faster. Test your dependencies first.
  • Re-check after changes. A new library or a bigger payload can shift the best setting, so tune again after significant releases.
  • Set a sensible timeout. A timeout left at the maximum lets a stuck function bill for minutes instead of failing fast.

Mistake 2: Letting failures retry forever

A failing function is expensive because every retry is billed like a normal invocation, and the default retry behavior is easy to overlook. A single bad message can trigger dozens of paid attempts, and a bad deployment can multiply that across a whole queue. [Add a story here if you have one: a poison message, a bug that looped overnight, or an unexpected bill.]

Three patterns cause most of the damage.

1. Async invocations retry by default

When something like S3 or EventBridge invokes your function asynchronously, Lambda retries failed events up to two more times by default. If your code isn't safe to run more than once, that means duplicate side effects as well as duplicate cost. Set the retry count and event age deliberately, and send failures somewhere you can inspect them.

MyFunction:
  Type: AWS::Serverless::Function
  Properties:
    EventInvokeConfig:
      MaximumRetryAttempts: 1
      MaximumEventAgeInSeconds: 3600
      DestinationConfig:
        OnFailure:
          Type: SQS
          Destination: !GetAtt FailedEventsQueue.Arn
Enter fullscreen mode Exit fullscreen mode

2. SQS messages with no dead-letter queue

When a Lambda function fails on an SQS message, the message becomes visible again after the visibility timeout and gets picked up again. Without a dead-letter queue (DLQ) and a limit on receives, a poison message can cycle indefinitely. Two settings fix this: a redrive policy that moves the message to a DLQ after a few attempts, and a visibility timeout of at least six times your function timeout, which is AWS's guidance for SQS-triggered functions.

OrdersQueue:
  Type: AWS::SQS::Queue
  Properties:
    VisibilityTimeout: 180   # 6x a 30-second function timeout
    RedrivePolicy:
      deadLetterTargetArn: !GetAtt OrdersDLQ.Arn
      maxReceiveCount: 3
Enter fullscreen mode Exit fullscreen mode

3. One bad record retries the whole batch

By default, if one message in a batch of 10 fails, all 10 return to the queue and get processed again, including the nine that succeeded. Enable partial batch responses so only the failed messages retry.

Events:
  Queue:
    Type: SQS
    Properties:
      Queue: !GetAtt OrdersQueue.Arn
      BatchSize: 10
      FunctionResponseTypes:
        - ReportBatchItemFailures
Enter fullscreen mode Exit fullscreen mode
def handler(event, context):
    failures = []
    for record in event["Records"]:
        try:
            process(record)
        except Exception:
            failures.append({"itemIdentifier": record["messageId"]})
    return {"batchItemFailures": failures}
Enter fullscreen mode Exit fullscreen mode

Also watch for loops

A function that writes to the same S3 bucket or queue that triggers it can invoke itself endlessly. Lambda has built-in recursive loop detection for some services, such as SQS, SNS and S3, but it is a safety net, not a design strategy. Use separate prefixes or buckets for input and output, and set a reserved concurrency limit on functions that could run away.

Also make your handlers idempotent, so a retry doesn't repeat side effects. Powertools for AWS Lambda has a ready-made idempotency utility.

Mistake 3: Building chatty architectures

A chatty architecture makes too many calls, hops or log lines to do a simple job, and each one adds a small charge or a slice of waiting time you pay for. The cost rarely shows up in one place, which is why it's easy to miss. [Add a real example here: a chain you simplified, or a log bill that surprised you.]

Lambda calling Lambda synchronously

When one function invokes another and waits for the result, both are billed during the wait. The caller pays for idle time, and a failure in the second function can trigger retries in the first. Two better options exist:

  • Use asynchronous events. Have the first function publish to SQS, SNS or EventBridge, and let the second consume it independently.
  • Use Step Functions for multi-step workflows. Orchestration moves the waiting out of your Lambda bill and gives you built-in retries and error handling. Standard Workflows are billed per state transition, so very high-volume, short workflows may fit Express Workflows better. Check the current pricing for your volume.

One item at a time

Functions that write to DynamoDB, call an API or send a message in a loop, one item at a time, spend most of their billed duration waiting on network round trips. Use batch operations where they exist, such as BatchWriteItem or SQS SendMessageBatch, and process items concurrently when order doesn't matter.

Polling instead of reacting

A scheduled function that runs every minute to check whether anything changed makes 1,440 invocations a day, most of them empty. If the source can emit an event, such as an S3 notification, a DynamoDB stream or an EventBridge rule, let it trigger the function instead. If you must poll a queue, use the SQS event source mapping rather than your own loop.

Logging everything

CloudWatch Logs charges for ingestion per GB, and for chatty functions that cost can exceed the compute cost. Common causes are debug logging left on in production, entire request payloads logged on every call, and log groups that never expire.

  • Log at INFO or above in production, and switch to DEBUG only while investigating.
  • Log identifiers and key fields, not whole payloads.
  • Set a retention period on every log group.
  • Consider structured logging with sampling, which Powertools for AWS Lambda supports.

Quick cost checklist

Run through these for every serverless workload before it reaches production.

  • [ ] Memory tuned with Power Tuning or Compute Optimizer, and arm64 tested
  • [ ] Function timeout set to a realistic value, not the maximum
  • [ ] Async retries and maximum event age configured on purpose
  • [ ] Every SQS queue has a DLQ, a redrive policy and a safe visibility timeout
  • [ ] Partial batch responses enabled for batch-based triggers
  • [ ] Handlers are idempotent
  • [ ] No synchronous Lambda-to-Lambda chains
  • [ ] Batch calls used instead of per-item loops
  • [ ] No scheduled polling where an event source exists
  • [ ] Log level, payload logging and retention reviewed
  • [ ] AWS Budgets alerts and Cost Anomaly Detection turned on

Conclusion

Most serverless overspending comes from three fixable habits: guessing memory, retrying without limits, and designing components that talk too much. None of the fixes needs a redesign, and each one improves reliability or speed along with cost.

Pick one workload this week, measure its memory with Power Tuning, check its queues for DLQs, and look at its log volume. You will likely find a quick win. [Close with a line about your own experience and invite readers to share their own cost mistakes in the comments.]

Further reading

Top comments (0)