Lambda Out of Memory Errors

How to detect memory pressure, identify leaks, and right-size your Lambda memory allocation using CloudWatch logs.

TL;DR

Lambda kills your function instantly when it exceeds its memory allocation. Look for Runtime exited with error: signal: killed or compare Max Memory Used against Memory Size in your REPORT lines. The fastest fix is increasing the memory allocation (which also gives you more CPU). For sustained fixes, stream large files instead of buffering them, paginate database queries, and watch for memory leaks across warm invocations. Use Lambda Power Tuning to find the optimal memory-to-cost ratio.

What happens when Lambda runs out of memory?

When a Lambda function attempts to use more memory than its configured allocation, the Linux kernel's OOM killer terminates the process immediately. There is no graceful shutdown, no opportunity to catch the error in a try/catch block, and no chance to flush logs or close connections. The invocation simply dies.

The error that appears in your CloudWatch logs is terse and easy to miss:

RequestId: abc-123 Error: Runtime exited with error: signal: killed
Runtime.ExitError

This message tells you the Linux kernel sent SIGKILL to your process. Unlike SIGTERM, SIGKILL cannot be intercepted or handled. Your function had zero warning before termination. In some cases, you may also see the REPORT line confirm the issue, where Max Memory Used equals or is extremely close to Memory Size:

REPORT RequestId: abc-123  Duration: 2500.00 ms  Billed Duration: 2500 ms  Memory Size: 256 MB  Max Memory Used: 254 MB

When Max Memory Used is within a few megabytes of Memory Size, your function is either already failing or about to. The REPORT line sometimes does not appear at all for hard OOM kills, because the process was terminated before Lambda could write it.

Unlike ECS or EC2, where you configure memory at the container or instance level and can run multiple processes, Lambda memory is set per function in a single configuration field. The range is 128 MB to 10,240 MB (10 GB) in 1 MB increments. This setting is critical because Lambda allocates CPU power proportionally to memory. At 1,769 MB, your function gets the equivalent of one full vCPU. Below that, you get a fractional CPU. Above it, you get additional CPU cores, up to six at 10,240 MB.

This proportional scaling means that increasing memory does not just give your function more RAM. It makes the function run faster overall, including CPU-bound operations like JSON parsing, compression, and encryption. In some cases, doubling the memory cuts execution time in half, which means the cost stays the same even though the per-millisecond price is higher.

Identifying memory errors in CloudWatch logs

Memory errors manifest in several ways depending on the runtime and how quickly memory was exhausted. Here are the patterns to search for in your CloudWatch log groups.

Direct OOM kill

The most common pattern. The function exceeded its memory allocation and the kernel killed it. You will see two lines in sequence:

RequestId: a1b2c3d4-5678-90ab-cdef-111122223333 Error: Runtime exited with error: signal: killed
Runtime.ExitError

If you see signal: killed without any other error preceding it (no unhandled exception, no timeout message), memory exhaustion is almost certainly the cause. Timeouts produce a different message: Task timed out after X seconds.

REPORT line comparison

Every successful Lambda invocation ends with a REPORT line that includes two memory fields. Compare them:

REPORT RequestId: abc-123  Duration: 3200.00 ms  Billed Duration: 3200 ms  Memory Size: 512 MB  Max Memory Used: 508 MB

In this example, the function used 508 MB out of a 512 MB allocation. That is 99.2% utilization. This invocation survived, but the next one with slightly more data will get killed. When Max Memory Used equals Memory Size exactly, the function was at the absolute limit and likely experienced degraded garbage collection performance before completing.

Near-OOM (memory pressure)

When Max Memory Used exceeds roughly 85% of Memory Size, the function is under memory pressure. It has not failed yet, but it is at risk. Garbage-collected runtimes like Node.js, Python, and Java spend increasing amounts of CPU time on garbage collection as memory fills up, which slows your function down even before it hits the hard limit. A function using 440 MB of a 512 MB allocation may be spending 15-20% of its CPU time on GC rather than doing useful work.

Runtime-specific errors

Some runtimes produce their own error messages before the kernel OOM kill hits:

// Node.js
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

// Python
MemoryError

// Java
java.lang.OutOfMemoryError: Java heap space

These runtime-level errors sometimes appear in logs before the SIGKILL, but not always. The Node.js V8 engine, for example, will attempt several rounds of aggressive garbage collection before admitting defeat and printing the FATAL ERROR line. If the kernel kills the process first, you will only see signal: killed with no V8 error.

Common causes of Lambda memory errors

Memory allocation too small for the workload

The simplest cause. Your function legitimately needs more memory than you have allocated. This is especially common when functions are created with the default 128 MB allocation and then start processing larger payloads over time. A function that worked fine with 100 KB API responses will fail when someone sends 10 MB.

Loading entire files into memory

Reading an entire S3 object into a buffer with s3.getObject().promise() or loading a complete DynamoDB scan result into an array. A 200 MB CSV file read into memory requires at least 200 MB just for the raw data, plus additional memory for the parsed representation. In Node.js, a string takes roughly twice the memory of its byte length due to UTF-16 encoding.

Memory leaks across warm invocations

Lambda reuses execution environments (containers) across invocations. Variables declared in global or module scope persist between invocations. If you push items into a global array, cache data in a module-level Map, or hold references to closed database connections, memory accumulates with each invocation. The first 100 invocations might work fine, but invocation 500 on the same container fails because the leaked memory has accumulated.

JSON.parse on large payloads

JSON.parse() must load the entire string into memory and then create a JavaScript object representation. For a 50 MB JSON file, you need memory for the raw string (around 100 MB in Node.js due to UTF-16), plus the parsed object (which can be 2-5x the original size depending on nesting and key repetition). A 50 MB JSON file can easily require 300-400 MB of working memory.

Image and PDF processing without streaming

Libraries like Sharp (Node.js) or Pillow (Python) load images into uncompressed bitmap format for processing. A 5 MB JPEG might decompress to 100 MB as a raw bitmap. PDF rendering libraries similarly expand compressed pages into full-resolution rasters. If you are processing multiple images or pages in a single invocation, memory usage multiplies quickly.

Too many concurrent SDK connections

Each AWS SDK connection maintains buffers for request and response data. If your function fans out to dozens of concurrent SDK calls (for example, writing to 50 DynamoDB items in parallel using Promise.all()), the combined buffer memory can be significant. Each in-flight HTTP request typically holds 64-256 KB of buffer space, so 100 concurrent requests consume 6-25 MB just in connection overhead.

How smplogs detects memory issues

When you analyze a Lambda log file with smplogs, the WASM engine automatically extracts Memory Size and Max Memory Used from every REPORT line, calculates utilization percentages, and flags functions at risk. If peak memory usage exceeds 80% of the allocation, you will see a finding like this:

MEDIUM Memory Pressure

Peak memory at 87% of 512MB allocation.

-> Consider increasing memory allocation to avoid OOM risk. Profile memory usage patterns.

The root cause analysis engine goes further. It correlates memory utilization trends with error patterns across your log file. If it detects that signal: killed errors cluster around invocations with high memory usage, it links the two findings together and surfaces a root cause rather than just isolated symptoms.

smplogs also performs anomaly detection on memory usage over time. If the first invocations in your log file use 200 MB and later invocations use 400 MB on the same function, the engine flags this as a potential memory leak. Gradual memory increases across warm invocations are one of the hardest patterns to catch manually, because each individual REPORT line looks normal. It is only the trend that reveals the problem.

The metrics card at the top of results shows peak memory utilization as a percentage, making it immediately visible whether your function is running with comfortable headroom or teetering at the edge of its allocation.

Step-by-step fixes

Fix 1: Increase memory allocation

The simplest and fastest fix. Go to your function's configuration and increase the memory setting. Remember that increasing memory also increases CPU proportionally, so CPU-bound functions get faster too. If your function currently uses 240 MB out of 256 MB, bumping it to 512 MB gives you breathing room and a significant CPU boost.

# AWS CLI
aws lambda update-function-configuration \
  --function-name my-function \
  --memory-size 512

# SAM template
Resources:
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      MemorySize: 512

The cost increase may be negligible or even zero. A function that runs for 2,000 ms at 256 MB costs the same as one that runs for 1,000 ms at 512 MB. If the extra CPU from doubling memory cuts your duration in half, you pay exactly the same total.

Fix 2: Use streaming for large files

Instead of loading entire S3 objects or HTTP responses into memory, process them as streams. This changes your memory usage from O(file_size) to O(chunk_size), regardless of how large the file is.

// Bad - loads entire file into memory
const response = await s3.getObject(params).promise();
const data = response.Body.toString();
processEntireFile(data);

// Good - streams the file in chunks
const stream = s3.getObject(params).createReadStream();
const rl = readline.createInterface({ input: stream });
for await (const line of rl) {
  processLine(line);
}

Fix 3: Process data in chunks

For DynamoDB queries and scans, always use pagination with ExclusiveStartKey rather than accumulating all items into a single array. For large arrays that need processing, use batched iteration. Instead of items.map(processItem) which creates a new array of equal size, use a for loop and process items in groups of 50-100 at a time, allowing the garbage collector to reclaim memory between batches.

Fix 4: Fix memory leaks in global scope

Audit all module-level variables that persist across invocations. Common leaks include caches that grow without eviction, arrays that accumulate log entries, event listeners that are re-registered on every invocation, and database connection pools that never close stale connections.

// Leak: grows by one item every invocation
const cache = [];
exports.handler = async (event) => {
  cache.push(event.data);  // never evicted
};

// Fixed: bounded cache with eviction
const cache = new Map();
const MAX_CACHE = 1000;
exports.handler = async (event) => {
  if (cache.size >= MAX_CACHE) {
    const oldest = cache.keys().next().value;
    cache.delete(oldest);
  }
  cache.set(event.id, event.data);
};

Fix 5: Configure runtime memory limits

For Node.js, you can set the V8 heap limit explicitly via the NODE_OPTIONS environment variable. By default, V8 sets its own heap limit which may not align with your Lambda memory configuration. Setting it explicitly ensures V8 runs garbage collection more aggressively before hitting the kernel OOM limit:

# For a 512 MB Lambda, reserve ~100 MB for the runtime itself
NODE_OPTIONS=--max-old-space-size=400

For Python, there is no direct equivalent, but you can use the resource module to set soft limits or use tracemalloc to monitor memory allocations during development and identify which objects consume the most memory.

Fix 6: Use /tmp for large intermediate data

Lambda provides between 512 MB and 10 GB of ephemeral storage at /tmp (configurable per function). Instead of holding large intermediate results in memory, write them to /tmp and read them back as needed. This is particularly useful for multi-step processing pipelines where you transform data through several stages. Write the output of each stage to disk instead of holding all stages in memory simultaneously. Be aware that /tmp persists across warm invocations on the same container, so clean up old files at the start of each invocation to avoid running out of disk space.

Right-sizing Lambda memory

Choosing the right memory allocation is a balancing act between cost, performance, and reliability. Too little memory and your function fails or runs slowly due to GC pressure. Too much memory and you are paying for resources you do not use. Here is how to find the sweet spot.

Lambda Power Tuning

The AWS Lambda Power Tuning tool is an open-source Step Functions state machine that invokes your function at every memory configuration (128 MB through 10,240 MB) and plots the cost-vs-duration curve. It reveals the exact point where adding more memory stops improving performance, which is the optimal allocation. Run it after every significant code change that affects memory usage.

The 1.5x rule of thumb

As a starting point, set memory to 1.5x your observed peak usage. If your function reports Max Memory Used of 340 MB, set it to 512 MB. This gives you a 50% buffer for input size variation, garbage collection spikes, and gradual increases over time. For functions with highly variable input sizes, use 2x the typical peak instead.

More memory can cost less

Lambda bills per GB-second (memory multiplied by duration). If doubling memory from 256 MB to 512 MB reduces your execution time from 4 seconds to 2 seconds, the cost is identical: 256 MB * 4s = 1,024 MB-seconds vs. 512 MB * 2s = 1,024 MB-seconds. In practice, the higher-memory version often costs slightly less because the extra CPU reduces duration by more than half. Always test rather than assuming.

The 1,769 MB baseline

At 1,769 MB, Lambda allocates exactly one full vCPU to your function. This is an important threshold for CPU-intensive workloads like JSON processing, image manipulation, encryption, and compression. Below this threshold, your function runs on a fractional CPU. If profiling shows your function is CPU-bound rather than I/O-bound, start with 1,769 MB and adjust from there.

Monitor trends over time

A function that uses 300 MB today might use 400 MB in three months as data volumes grow or new features are added. Set up CloudWatch alarms on the maximum statistic of the Max Memory Used metric. Alert at 75% utilization so you can proactively increase the allocation before OOM errors start. CloudWatch Lambda Insights provides memory utilization graphs out of the box if you enable the Lambda Insights extension layer on your function.

Use Lambda Insights for historical data

Lambda Insights is a CloudWatch feature that collects system-level metrics including memory utilization over time. Enable it by adding the Lambda Insights extension layer to your function. It provides a pre-built dashboard showing memory usage as a percentage of the allocation, plotted across invocations. This makes it easy to see whether your function has a stable memory footprint or a gradually increasing one that suggests a leak.

Worried about memory pressure? Drop your CloudWatch JSON into smplogs to see peak memory usage, detect near-OOM invocations, and track memory trends across your logs.

Try it free

Related guides