What it means
Every Lambda invocation runs inside a Linux container with a hard memory limit set by your MemorySize configuration. That limit is enforced by the kernel's control groups, not by the runtime, and the kernel's enforcement mechanism is blunt: when a process in the cgroup tries to allocate past the limit, the OOM killer sends it SIGKILL.
SIGKILL is the one signal a process cannot catch, block, or handle. There is no shutdown hook, no finally block, no exception handler, and no opportunity to flush a log line. The process is removed from the scheduler between one instruction and the next. This is exactly why the failure is so disorienting the first time you meet it — every other Lambda failure leaves a stack trace pointing at a line of your code, and this one leaves a gap where your logs used to be.
What Lambda writes afterwards, Runtime exited with error: signal: killed, is the supervisor's observation, not your function's report. The supervisor noticed the runtime process was gone and recorded how it went. It has no idea what your code was doing at the time, which is why the message contains nothing useful about your program.
The useful information is in two places instead. The REPORT line's Max Memory Used equals Memory Size exactly — the allocation was still climbing when the process died, so the high-water mark is the ceiling. And your own last log line marks how far the work got before the allocation that crossed the line. Those two facts together tell you both that it was memory and where the memory went, which is why reading them next to each other — rather than hunting for a REPORT line among thousands, several screens away from the log lines it belongs to — is the difference between a two-minute diagnosis and an afternoon.
CloudWatch’s Errors metric: The error escaped your handler, so Lambda reports the invocation as failed and CloudWatch’s Errors metric counts it. LogStitch classified the invocation above as uncaught.
What it looks like in CloudWatch
This is the shape the failure arrives in: the lines of one invocation scattered among everything else the log group received at the same moment.
CloudWatch Logs Before
6 raw lines, in the order CloudWatch delivered them.
LogStitch After
The same lines, grouped into the invocation they belong to.
ErrorMemory ceiling3f7a91cc-4f10-4c11-8f0b-1b2d3e4f5a01
- 04:12:08.114PLAT
START RequestId: 3f7a91cc-4f10-4c11-8f0b-1b2d3e4f5a01 Version: $LATEST
- 04:12:08.118INFO
INFO Export requested range=2026-07-01..2026-07-31
- 04:12:08.402INFO
INFO Fetching ledger objects prefix=exports/2026-07
- 04:12:09.885INFO
INFO Loaded 1 of 3 objects bytes=184320114
- 04:12:11.204
RequestId: 3f7a91cc-4f10-4c11-8f0b-1b2d3e4f5a01 Error: Runtime exited with error: signal: killed
- 04:12:11.204PLAT
REPORT RequestId: 3f7a91cc-4f10-4c11-8f0b-1b2d3e4f5a01 Duration: 3090.44 ms Billed Duration: 3091 ms Memory Size: 512 MB Max Memory Used: 512 MB
The panel on the right is generated by running the excerpt on the left through the same parser that powers the free web stitcher — it is what the tool actually produces for this input, not an illustration of it.
How to confirm it from the logs
The decisive evidence is on the REPORT line, not the error line. Compare Max Memory Used against Memory Size: for an OOM kill they are equal, because the process was killed the instant it crossed the ceiling. That equality is the confirmation — the signal: killed text alone can also mean the sandbox was reclaimed for another reason.
The second tell is the absence of anything. No stack trace, no [ERROR] line, no exception name. Your own logs simply stop, mid-work, at whatever the last thing you wrote was. If you have a stack trace, this is not an OOM kill.
Causes, most likely first
The function loads a whole payload into memory at once
Look at the last line before the logs stop, and at what your code does next. Reading an entire S3 object with getObject().Body.transformToString(), parsing a large JSON document, or collecting a full query result into an array all hold the complete payload in memory. If the size of that payload tracks the size of the input, memory use is proportional to input and the ceiling is one large record away.
The memory allocation is simply too low for the workload
Check Max Memory Used across several successful invocations of the same function. If the healthy ones already sit above about 85% of Memory Size, there is no headroom and any variation in input tips it over. A function that normally reports 241/256 MB is not comfortable — it is one busy request from being killed.
Memory accumulates across invocations in a warm container
Compare Max Memory Used on a cold start against a container that has served many requests. Lambda reuses execution environments, so anything at module scope — a cache that never evicts, an array appended to on every call, an event listener registered per invocation — survives between invocations and grows. Rising memory across consecutive requests on the same container is the signature.
A dependency buffers far more than you asked it to
Check whether a library sits between you and the data. Image processing, PDF generation, spreadsheet parsing, and some database drivers materialise the whole working set regardless of how you feed them. If your own code looks streaming but memory still spikes, the buffering is happening a layer down.
Fixes
Raise the memory allocation — and get more CPU with it
This is the fastest fix and often the correct one. Lambda scales CPU with memory, so a function given more memory frequently finishes faster as well, which can leave the bill flat or lower even though the per-millisecond rate went up. Set the value from evidence: take the highest Max Memory Used you have observed and leave roughly 30% headroom above it.
yaml# AWS SAM
Resources:
ReportFunction:
Type: AWS::Serverless::Function
Properties:
MemorySize: 1024 # was 512; observed peak was 512/512
Stream instead of buffering
If memory use tracks input size, no allocation is safe for long. Process the data as it arrives so peak memory depends on your chunk size rather than on the payload.
jsimport { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { createInterface } from "node:readline";
const s3 = new S3Client({});
export const handler = async (event) => {
const object = await s3.send(new GetObjectCommand({
Bucket: event.bucket,
Key: event.key
}));
// One line in memory at a time, not the whole object.
const lines = createInterface({ input: object.Body });
let count = 0;
for await (const line of lines) {
await handleRow(JSON.parse(line));
count += 1;
}
return { count };
};
Bound anything that lives at module scope
Module-scope state survives between invocations by design — that is what makes connection reuse work. It becomes a leak when it grows without limit. Give every cache a maximum size, and register listeners once rather than per invocation.
js// Wrong: grows forever across every invocation this container serves.
const cache = new Map();
// Right: bounded, so a warm container has a memory ceiling of its own.
const MAX = 500;
const cache = new Map();
function remember(key, value) {
if (cache.size >= MAX) cache.delete(cache.keys().next().value);
cache.set(key, value);
}
Also seen as
The same underlying failure, worded differently by a different runtime, SDK version, or logging layer. All of these land here — there is no separate page for each phrasing.
Related errors
Errors that show up alongside this one, or that people mistake for it.
References
- AWS Lambda Developer Guide — Configuring function memory
- AWS Lambda Developer Guide — Troubleshoot invocation issues
LogStitch finds this automatically, across every invocation in your account.
Paste a log excerpt into the free web stitcher and see it grouped, classified, and measured in your browser — nothing is uploaded. Or run the Mac app against your own AWS profiles and get the same view over every function you own.