---
title: The Frugal Approach to AWS Lambda Costs
description: With usage-billed compute services like AWS Lambda, cost optimization starts with your code. It's not just that applications run on the functions; applicat
image: https://frugal.co/hubfs/blog-images/featured-Jul-23-2026-12-48-23-1457-PM.jpg
---

[![company-logo](https://cdn.prod.website-files.com/686b121c919ed2b639e30952/686b28b7662f765388c4c0f2_Layer_1.svg)](https://frugal.co/)

[Product](https://frugal.co/product) [Pricing](https://frugal.co/pricing)

Company

[About Us](https://frugal.co/about) [Security](https://frugal.co/security)

Resources

[Blog](https://frugal.co/blog) [Library](https://frugal.co/resources)

[Contact](https://frugal.co/contact)

[Book Demo](https://frugal.co/) [Get a Free Audit](https://frugal.co/audit)

Cost Engineering

AWS

The Frugal Approach to AWS Lambda Costs

![Craig Conboy](https://frugal.co/hs-fs/hubfs/Craig%20Conboy%20Headshot.png?width=60&height=60&name=Craig%20Conboy%20Headshot.png)

Craig Conboy

 July 23, 2026

With usage-billed compute services like AWS Lambda, cost optimization starts with your code. It's not just that applications run on the functions; applications spend. Every invocation, every configured megabyte, every billed millisecond contributes to your bill.

Taking an application- and code-centric approach to cost reduction means understanding what your code costs before and after each invocation. Here's a practical walk-through of what to look for and how to optimize Lambda costs at the code and configuration level.

| Cost Trap | Efficiency Pattern |
| --- | --- |
| Over-provisioned function memory | Right-size memory to observed usage plus headroom |
| x86 functions where ARM64 works | Migrate to Graviton (arm64) for ~20% lower duration cost |
| Excessive function timeout | Set timeouts just above observed worst-case runtime |
| Synchronous function-to-function chains | Invoke asynchronously (Event, SQS, Step Functions) |
| Wasteful warming schedules | Warm only what traffic justifies, when it justifies it |
| Over-provisioned provisioned concurrency | Right-size provisioned concurrency to peak utilization |
| Processing events one at a time | Batch SQS/Kinesis records per invocation |
| Retry storms and recursive loops | Bound retries, add DLQs, break event cycles |
| Verbose logging from functions | Log less—CloudWatch ingestion often out-bills compute |
| Paying for idle wait | Don't block on slow externals; hand off and call back |

## Attribute the Costs

Start with your bill. Break down costs by function, application, and environment to understand where spend concentrates. Lambda charges across three dimensions: requests (~$0.20 per 1M invocations), duration (~$0.0000166667 per GB-second on x86, ~20% less on ARM64), and provisioned concurrency (~$0.0000041667 per GB-second, billed whether invoked or not). Duration is billed in 1 ms increments, and CPU scales with the memory setting—so memory is really the price knob for the whole function.

Don't stop at the Lambda line item. Lambda functions generate downstream costs that land on other bills: CloudWatch Logs ingestion (~$0.50/GB) for everything your functions write to stdout, NAT gateway charges for VPC-attached functions, and data transfer for cross-region calls. A chatty function can cost more in log ingestion than in compute.

Attribute costs down to specific functions and code paths. Which functions are over-provisioned on memory? Which run warm 24/7 for traffic that only arrives 9-to-5? Which spend most of their billed milliseconds waiting on someone else's API? This granular attribution reveals which of the 10 efficiency patterns below deliver the highest impact.

---

## Tackling Duration Costs

Duration cost = memory setting × billed milliseconds. Both factors are in your control.

**Right-size memory to observed usage.**

**Migrate to ARM64 (Graviton).**

**Don't pay full compute rates to wait.**

### Right-size function memory

**Cost impact: Duration**

Lambda bills for configured memory, not used memory. A function configured at 3,008 MB that peaks at 400 MB pays 7.5x more per millisecond than it needs to. Check the `Max Memory Used` line in your function's CloudWatch logs report, or CloudWatch Lambda Insights:

```
REPORT RequestId: 5e2a...  Duration: 487 ms  Billed Duration: 488 ms
Memory Size: 3008 MB  Max Memory Used: 402 MB
```

```
# Bad: memory set by copy-paste from another function
MyIngestFunction:
  Type: AWS::Serverless::Function
  Properties:
    MemorySize: 3008   # peaks at 402 MB
    Timeout: 900

# Good: observed peak plus headroom
MyIngestFunction:
  Type: AWS::Serverless::Function
  Properties:
    MemorySize: 512    # 402 MB peak + ~25% headroom
    Timeout: 60
```

One caveat: CPU scales with memory, so for CPU-bound functions a bigger setting can finish faster and cost the *same or less*. Measure before and after—[AWS Lambda Power Tuning](https://github.com/alexcasalboni/aws-lambda-power-tuning) automates the sweep. For the common case (I/O-bound handlers waiting on databases and APIs), extra memory buys nothing.

**Benefit:** 20-60% reduction in duration costs for over-provisioned functions.

### Migrate to ARM64 (Graviton)

**Cost impact: Duration**

ARM64 duration is priced ~20% below x86\_64, and for most runtimes it benchmarks the same or faster. If your function is pure Python/Node/Java with no architecture-specific native binaries, this is close to free money:

```
# Bad: default architecture
Architectures:
  - x86_64

# Good: Graviton
Architectures:
  - arm64
```

Check native dependencies (anything with compiled wheels or `node-gyp`) build for `linux/arm64`, redeploy, and compare a week of REPORT lines. That's the whole migration for most functions.

**Benefit:** ~20% reduction in duration costs, often with equal or better latency.

### Don't pay to wait

**Cost impact: Duration**

Lambda bills wall-clock time. A function that calls a slow third-party API and blocks for 20 seconds pays for 20 seconds of compute to do nothing:

```
# Bad: poll-and-sleep inside the function (billed the whole time)
def handler(event, context):
    job_id = start_render(event['doc'])
    while not render_complete(job_id):
        time.sleep(5)          # you are paying for this sleep
    return fetch_result(job_id)
```

```
# Good: hand off and let the callback resume the workflow
# Step Functions .waitForTaskToken pattern
def start_handler(event, context):
    start_render(event['doc'], callback_token=event['taskToken'])
    # function exits immediately; billing stops

def complete_handler(event, context):
    # invoked by the render service's completion webhook
    sfn.send_task_success(
        taskToken=event['token'],
        output=json.dumps(event['result'])
    )
```

Step Functions charges per state transition, not per second—waiting is nearly free there and expensive in Lambda.

**Benefit:** Eliminates billed idle time; long waits drop from function duration to ~$0.

---

## Tackling Invocation & Orchestration Costs

How functions call each other matters as much as what they do.

**Break synchronous invocation chains.**

**Batch event processing.**

**Bound retries and break loops.**

### Break synchronous invocation chains

**Cost impact: Duration (double-billed)**

When function A invokes function B synchronously (the default `RequestResponse` type) and waits for the result, you pay for A's compute during B's entire execution. Chain three functions and the top of the chain pays three times.

```
# Bad: synchronous chain — A is billed while B runs
def handler_a(event, context):
    response = lambda_client.invoke(
        FunctionName='function-b',
        InvocationType='RequestResponse',   # A blocks and bills
        Payload=json.dumps(payload)
    )
    return process(response['Payload'].read())
```

```
# Good: async invoke when A doesn't need the answer
def handler_a(event, context):
    lambda_client.invoke(
        FunctionName='function-b',
        InvocationType='Event',             # fire and forget
        Payload=json.dumps(payload)
    )
    return {'status': 'queued'}
```

When A genuinely needs B's result, that's not a Lambda-to-Lambda call—that's a workflow. Use Step Functions, or put SQS between the stages. And if B is just shared code, make it a library or a Lambda layer instead of a network hop you pay for twice.

**Benefit:** Eliminates double/triple billing on chained invocations.

### Batch event processing

**Cost impact: Request and Duration**

Every invocation pays the request charge plus cold-start amortization plus per-invocation overhead (init, connection setup). Processing SQS messages one at a time multiplies all of it:

```
# Bad: one message per invocation
Events:
  Queue:
    Type: SQS
    Properties:
      Queue: !GetAtt JobsQueue.Arn
      BatchSize: 1

# Good: batch messages per invocation
Events:
  Queue:
    Type: SQS
    Properties:
      Queue: !GetAtt JobsQueue.Arn
      BatchSize: 100
      MaximumBatchingWindowInSeconds: 30
      FunctionResponseTypes:
        - ReportBatchItemFailures   # partial-failure handling
```

```
# Handler processes the batch and reports only the failures
def handler(event, context):
    failures = []
    for record in event['Records']:
        try:
            process(json.loads(record['body']))
        except Exception:
            failures.append({'itemIdentifier': record['messageId']})
    return {'batchItemFailures': failures}
```

Same pattern applies to Kinesis and DynamoDB Streams via `BatchSize` and batching windows.

**Benefit:** 10-100x fewer invocations for high-volume queues; connection reuse across records.

### Bound retries and break loops

**Cost impact: Request and Duration (unbounded)**

Failure handling is where serverless bills go exponential. Async invocations retry twice by default, SQS redelivers until `maxReceiveCount`, and a recursive trigger—a function writing to the S3 bucket or queue that triggers it—runs until something breaks or the invoice arrives.

```
# Bad: S3-triggered function writes back to the same bucket
def handler(event, context):
    for record in event['Records']:
        key = record['s3']['object']['key']
        thumb = make_thumbnail(bucket, key)
        s3.put_object(Bucket=bucket, Key=f"thumb-{key}", Body=thumb)
        # "thumb-*" objects re-trigger this function. Forever.
```

```
# Good: scope the trigger so outputs can't re-trigger,
# and give failures somewhere to land
Events:
  Upload:
    Type: S3
    Properties:
      Bucket: !Ref UploadBucket
      Events: s3:ObjectCreated:*
      Filter:
        S3Key:
          Rules:
            - Name: prefix
              Value: uploads/    # outputs go to thumbs/, no cycle
DeadLetterQueue:
  Type: SQS
  TargetArn: !GetAtt FailuresDLQ.Arn
```

For SQS sources, set a `maxReceiveCount` (3-5) with a dead-letter queue, and make handlers idempotent so a retry is safe rather than compounding. Lambda's recursive-loop detection catches some common cycles, but only the patterns it knows about—don't outsource this one.

**Benefit:** Converts unbounded failure cost into a bounded, visible dead-letter queue.

---

## Tackling Idle Capacity Costs

Provisioned concurrency and warmers charge you for readiness, not work. Both drift out of sync with actual traffic.

### Right-size provisioned concurrency

**Cost impact: Provisioned Concurrency**

Provisioned concurrency bills per GB-second whether requests arrive or not. Teams set it during a launch, traffic settles, and the setting stays. Check `ProvisionedConcurrencyUtilization` in CloudWatch—if peak utilization sits at 15%, you're paying for 6x the warm capacity you use:

```
# Bad: launch-day setting, never revisited
ProvisionedConcurrencyConfig:
  ProvisionedConcurrentExecutions: 100   # peak utilization: 15%

# Good: sized to observed peak plus headroom
ProvisionedConcurrencyConfig:
  ProvisionedConcurrentExecutions: 20
```

Better still, schedule it. Application Auto Scaling can scale provisioned concurrency on utilization or on a cron, so the weekend runs at zero:

```
aws application-autoscaling put-scheduled-action \
  --service-namespace lambda \
  --resource-id function:checkout:live \
  --scheduled-action-name business-hours-up \
  --schedule "cron(0 8 ? * MON-FRI *)" \
  --scalable-target-action MinCapacity=20,MaxCapacity=20
```

**Benefit:** Cuts provisioned concurrency spend to match real peak usage while keeping cold-start protection where it matters.

### Eliminate wasteful warming

**Cost impact: Request and Duration**

Warmer plugins (scheduled EventBridge pings keeping containers warm) predate provisioned concurrency and linger in serverless configs like an appendix. The wasteful patterns show up in three flavors: warmers firing all night for functions with 9-to-5 traffic, warmers targeting functions nobody calls anymore, and warmers running *alongside* provisioned concurrency on the same function—paying twice for the same warmth.

```
# Bad: warming an internal tool at 3am, every day, forever
custom:
  warmup:
    default:
      enabled: true
      events:
        - schedule: rate(5 minutes)   # 8,640 warm-up invocations/month

# Good: warm during business hours only — or delete the warmer
custom:
  warmup:
    default:
      enabled: true
      events:
        - schedule: cron(0/5 8-18 ? * MON-FRI *)
```

Before tuning a warmer's schedule, ask the prior question: does this function's cold-start rate actually hurt anyone? Warm-up invocations are cheap individually and pointless in aggregate.

**Benefit:** Eliminates warming spend on off-hours, unused functions, and functions already covered by provisioned concurrency.

---

## Tackling Downstream Costs

### Log less from functions

**Cost impact: CloudWatch Logs (adjacent bill)**

Everything a function writes to stdout is ingested by CloudWatch Logs at ~$0.50/GB—about 30x the monthly cost of *storing* that gigabyte. A function printing a 2 KB debug blob on each of 50M monthly invocations generates ~100 GB of ingestion: $50/month of logging for a function whose compute might cost $20.

```
# Bad: debug-everything in production
def handler(event, context):
    print(f"EVENT: {json.dumps(event)}")          # full payload, every call
    result = process(event)
    print(f"RESULT: {json.dumps(result)}")
    return result

# Good: structured, leveled, and quiet by default
logger = logging.getLogger()
logger.setLevel(os.environ.get("LOG_LEVEL", "WARNING"))

def handler(event, context):
    result = process(event)
    logger.info("processed", extra={"id": event["id"], "ms": result.elapsed})
    return result
```

Set the log level from the environment so production runs at WARNING while you keep DEBUG for dev. For high-volume functions, also set a short retention on the log group (the default is *never expire*) and consider Lambda's JSON log format with application-level filtering so you only ship what you'd actually query.

**Benefit:** 80-95% reduction in log ingestion costs for chatty functions.

---

## A Note on Timeouts

**Cost impact: Risk containment**

One more configuration worth a pass: function timeout. Teams set `Timeout: 900` (the 15-minute max) "to be safe" on functions whose worst observed runtime is 4 seconds. The timeout is a hard compute kill—it's the cap on what a stuck invocation can cost you. With a 900-second timeout, a hung dependency turns every invocation into 225x the normal spend until someone notices; at high concurrency that's a very expensive way to discover an outage.

```
# Bad: maximum "just in case"
Timeout: 900     # observed p100 runtime: 4s

# Good: worst-case plus margin
Timeout: 15
```

This one's about bounding the blowup, not trimming the steady-state bill—the direct savings are $0 until the bad day arrives, at which point they're the whole point.

---

## Closing Thoughts

Cost-effective Lambda requires two steps. First, let observed costs guide what needs optimization. Attribute your bill down to specific functions and code paths—which are over-provisioned on memory, which chain synchronously, which run warm around the clock for business-hours traffic, which spend their billed milliseconds sleeping on someone else's API. This granular attribution reveals which of the 10 efficiency patterns above matter most for your workloads. Second, apply the optimizations that address your cost concentrations: right-size memory (20-60% duration savings), move to ARM64 (~20% off), break synchronous chains (eliminates double billing), batch event processing (10-100x fewer invocations), bound retries before they go exponential, right-size provisioned concurrency and warmers to real traffic, and quiet down your logging before CloudWatch out-bills your compute.

You don't lose performance or reliability. You gain precision. Serverless made compute a per-millisecond purchasing decision—treat each configuration line as having a price tag and optimize based on measured impact.

*Looking for help with cost optimizations like these? [Book a Demo](https://frugal.co) or [Explore the Sandbox](https://frugal.co/sandbox) to see Frugal in action. Frugal attributes AWS Lambda costs to your code, finds inefficient usage, provides Frugal Fixes that reduce your bill, and helps keep you out of cost traps for new and changing code.*

 Share Link

[Back to Top](https://frugal.co/blog/ai-or-engineer-cloud-cost-savings-start-with-the-bill-and-end-in-the-code)

![background Image](https://frugal.co/hubfs/697fa79222025cf13e7bec1e_799f61dfa4790fe0e40a2962d5468335_Group%202040.avif)

![Robo Image](https://frugal.co/hubfs/697fadb140def08f5fc80ed3_Group%202042.avif)

### Take Frugal for a live test drive

 Explore how Frugal scans your code and cloud services to find waste, recommend optimizations, and generate ready-to-use fixes in a secure, read-only environment.

[Enter Sandbox](https://auth.frugal.co/sign-up)

![background Image](https://frugal.co/hubfs/697fa936779f0fd839545616_Group%202041.avif)

![robo image](https://frugal.co/hubfs/697fa79222025cf13e7bec1e_799f61dfa4790fe0e40a2962d5468335_Group%202040.avif)

![frugal image](https://cdn.prod.website-files.com/686b121c919ed2b639e30952/686c44713462acbdaab4a82d_f8418228347e59c4cabb651eb7f5e72b_Frame%20291022.avif)

 An Intelligent Application Cost Engineering platform that optimizes code to reduce cloud costs automatically - empowering engineers without slowing development

[Blog](https://frugal.co/blog) [Contact](https://frugal.co/contact) [careers](https://frugal.co/careers) [About Us](https://frugal.co/about)

[Privacy Policy](https://frugal.co/privacy-policy) [Terms of Use](https://frugal.co/terms-and-condition) [Trust Centre](https://trust.frugal.co/) [Active Status](https://status.frugal.co/)

[X](https://x.com/frugalaico) [LinkedIn](https://www.linkedin.com/company/frugalco/)

[Featured in AI Directory](https://aidirectory.wiki)

[Privacy Policy](https://frugal.co/privacy-policy) [Terms of Use](https://frugal.co/terms-and-condition) [Trust Centre](http://trust.frugal.co/) [Active Status](http://status.frugal.co/)

Copyright Frugal AI Inc.

©2025

```json
{
  "@context" : "https://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Craig Conboy",
    "url" : "https://frugal.co/blog/author/craig-conboy"
  },
  "datePublished" : "2026-07-23T12:48:23.000Z",
  "headline" : "The Frugal Approach to AWS Lambda Costs",
  "image" : [ "https://frugal.co/hubfs/blog-images/featured-Jul-23-2026-12-48-23-1457-PM.jpg" ],
  "mainEntityOfPage" : {
    "@id" : "https://frugal.co/blog/the-frugal-approach-to-aws-lambda-costs",
    "@type" : "WebPage"
  },
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "url" : "https://frugal.co/hubfs/Frugal%20Logo%20Transparent%20Black.png"
    }
  }
}
```