Fix AWS Lambda Runtime Exited with Signal Killed (SIGKILL)
"Runtime exited with error: signal: killed" means AWS Lambda's own container manager killed your function's process from the outside — almost always because it used more memory than you allocated, or because something inside it (a stuck extension, a runaway connection, a full /tmp folder) forced Lambda's hand. The counterintuitive part: raising the memory setting fixes this more often than the CloudWatch console tells you it should, because Lambda hides your function's real memory ceiling unless you go looking for it.
Jake's shop had a customer waiting on a trade-in phone that needed its photos pulled off before he could wipe it. The little AWS Lambda function that resizes and backs up customer photos had been running fine for months. That afternoon it just stopped, mid-batch, with nothing in the logs except one ugly line: Runtime.ExitError: RequestId: ... Error: Runtime exited with error: signal: killed. No stack trace. No line number. Nothing his code had actually thrown. Just gone.
That's the trap with this particular error. Every other Lambda failure at least points a finger at your code. This one doesn't, because your code didn't throw it — the Lambda execution environment did, from the outside, the way you'd unplug a machine rather than ask it nicely to stop. Understanding why Lambda reaches for that off-switch is most of the battle, and it comes down to a short list of causes that this post walks through in order, cheapest fix first.
What "signal: killed" actually means
Every Lambda function runs inside a small, isolated sandbox — Amazon calls it the execution environment. Think of it like a shipping container with a hard limit stenciled on the side: this much memory, this much CPU, this much temporary disk space, and no more. Your code runs the way it always would, right up until it tries to use more of one of those resources than the container allows. At that point, the underlying Linux kernel — or Lambda's own supervisor process watching over it — doesn't ask your code to clean up and exit gracefully. It sends a SIGKILL signal, which is the Linux equivalent of pulling the plug. There's no catching it, no try/finally block that runs, no last log line your code gets to write. That abruptness is exactly why the error you see back in CloudWatch just says "killed" instead of naming a cause: from Lambda's point of view, the process didn't fail — it was terminated.
errorType: "Runtime.ExitError" with the message Error: Runtime exited with error: signal: killed is Lambda translating that kernel-level kill into the response it hands back to whatever invoked the function. If you invoked the function directly (say, with the AWS CLI or through the console's Test button), you'll see this in the response payload. If something else triggered it — an S3 upload, an SQS queue, EventBridge — you'll only see it in CloudWatch Logs, or in a dead-letter queue if you've configured one, because asynchronous invocations don't hand errors back to a waiting caller the same way.
⚠️ What this actually breaks
A killed invocation still burns your invoke count and, for synchronous callers like API Gateway, still returns a response — just an ugly one, a 200 status with an X-Amz-Function-Error header rather than the 4xx/5xx you might expect. If nothing downstream is checking for that header, a killed Lambda function can silently drop customer data instead of loudly failing. That's worse than a timeout, because a timeout at least looks like an error to most client code.
Which of these five is happening to you
"Signal: killed" is a symptom, not a diagnosis — five genuinely different problems all produce the exact same log line. Before changing anything, pin down which one you actually have. Pull up a recent failed invocation in CloudWatch Logs and check it against the table below.
| What you see in the logs | Likely cause | Jump to fix |
|---|---|---|
| "Max Memory Used" in the REPORT line is close to or equal to the configured memory | Out-of-memory kill | Raise memory |
| Memory Used looks nowhere near the limit, function opens DB connections or HTTP clients | File descriptor / thread exhaustion | Fix connection reuse |
| Function writes large files, downloads models, or unzips archives before failing | /tmp (ephemeral storage) full | Resize /tmp |
| Function uses a Lambda Extension (observability agent, custom telemetry), kill happens right at the end of execution | Extension missed its shutdown window | Fix graceful shutdown |
| Function is packaged as a container image, kill happens on nearly every cold start | Image build/init problem | Check the image |
♂️ Jake's Reality Check
"I gave it the maximum memory already — 10,240 MB, the whole limit. It still gets killed. How is that even possible?"
Because the memory setting isn't your only ceiling. Lambda functions also share a hard limit of 1,024 open file descriptors and threads for the whole execution environment, no matter how much memory you buy. If your code is leaking database connections, more memory just buys you a slightly longer runway before you hit the same wall.
Fix 1: the out-of-memory kill (the common case)
This is the one worth ruling out first, because it's also the cheapest to fix. AWS Lambda allocates CPU power in direct proportion to how much memory you configure, and every function's memory can be set anywhere between 128 MB and 10,240 MB, in 1-MB increments — at 1,769 MB, a function gets the equivalent of one full vCPU. When your function's actual memory use crosses whatever number you've set, the execution environment kills the process outright rather than letting it swap or slow down, because there's no swap space to fall back on inside the sandbox.
- Open CloudWatch Logs for the function and find the log stream for the invocation that failed. Look for the line that starts with
REPORT RequestId:. - Compare two numbers on that line: "Memory Size" (what you configured) against "Max Memory Used" (what the invocation actually consumed). If they're equal, or within a few megabytes of each other, you've found your cause — that invocation ran right up against the wall and Lambda cut it off.
- Go to the Lambda console, open the function, choose the Configuration tab, then General configuration, then Edit.
- Raise the Memory value. Don't just nudge it — if you were at 128 MB and topping out, jump to 512 MB or 1,024 MB rather than 192 MB, since a small bump often just delays the same kill on a slightly larger input.
- Save, then re-run the same job that failed before, watching the REPORT line again to confirm the new "Max Memory Used" sits comfortably below the new limit — with room to spare, not right at the edge.
The same change is a one-line AWS CLI command if you'd rather not click through the console: aws lambda update-function-configuration --function-name my-function --memory-size 1024. If you manage the function through AWS SAM or CloudFormation, it's the MemorySize property on the function resource.
✅ Why this is the one to use first
Memory is genuinely the main lever AWS gives you for a Lambda function's performance — it's not just a bandage. Extra memory doesn't just prevent the kill; it proportionally raises the CPU allotment too, so functions that are slow because they're CPU-starved (image processing, PDF generation, anything doing real number-crunching) frequently finish faster and cheaper at a higher memory setting than at a lower one, even though the per-millisecond price goes up. The lowest bill on paper isn't always the lowest bill in practice.
If you don't want to guess at the right number, AWS publishes an open-source tool called AWS Lambda Power Tuning that runs your function at several memory settings concurrently, using AWS Step Functions, and reports back which setting gives you the best balance of speed and cost. It's overkill for a function you touch once a year, but worth the ten minutes for anything running in production at real volume.
Fix 2: file descriptor and thread exhaustion (when memory looks fine)
Here's the case that trips people up worst, because it looks exactly like an out-of-memory kill in the logs — same "Runtime.ExitError," same "signal: killed" text — while the REPORT line's "Max Memory Used" sits nowhere near the configured limit. A file descriptor is Linux's internal name-tag for any open connection: a database connection, an open file, a socket to another service. Every execution environment gets a hard ceiling of 1,024 file descriptors and threads combined, and that ceiling doesn't move no matter how much memory you buy.
The classic mistake: opening a connection inside the handler
Lambda reuses "warm" execution environments across invocations to save on cold-start time. That's a gift if you use it right, and a slow-motion disaster if you don't. If your database connection, HTTP client, or Redis connection gets created inside the function handler — the part that runs on every single invocation — and never gets closed, every warm invocation opens one more connection on top of whatever's already open from the last one. Nothing looks wrong for the first dozen calls. Then, somewhere around invocation nine hundred and something, the environment hits its file-descriptor ceiling and gets killed, with no warning and no memory pressure at all.
⚠️ What this actually breaks
Database connection leaks don't just kill the leaking function — they can exhaust the connection pool on the database side too, since each abandoned Lambda connection can sit there taking up a slot on your RDS instance until it times out on its own. One misbehaving function can quietly starve every other service sharing that database.
The fix is to move connection setup outside the handler function entirely, into the top level of your file, so it only runs once per execution environment — during what Lambda calls the "init" phase — instead of once per invocation. Subsequent warm invocations reuse the same connection instead of opening a new one. If your runtime or driver supports it, use a keep-alive setting on your HTTP clients too, so idle connections get cleaned up on a schedule instead of accumulating.
♂️ Jake's Reality Check
"So it worked fine for two weeks and then just... stopped? Nothing changed on my end."
That's the signature of a leak, not a bug that was always broken. A leak needs volume to become visible — a function that only runs a handful of times a day might take months to reach the 1,024-descriptor ceiling, which is exactly why it looks like it "used to work."
How to confirm it without guessing
Turn on Lambda Insights for the function — it's an extension you enable from the Configuration tab under Monitoring and operations tools, and it writes detailed system-level metrics to a log group named /aws/lambda-insights. Two metrics matter here specifically: fd_use, the number of file descriptors currently open, and fd_max, the ceiling (1,024). If you query CloudWatch Logs Insights on that log group and see fd_use climbing invocation after invocation on the same warm environment, that's the leak, caught in the act rather than guessed at.
Debugging from the command line, without opening the console
Clicking through the CloudWatch console works fine for a single failure you already have in front of you. If a function is only killed intermittently and you need to catch it while retesting, waiting on page loads every time gets old fast. Two AWS CLI habits save most of that back-and-forth.
The first is built straight into the invoke command. Passing --log-type Tail to aws lambda invoke returns up to 4 KB of the most recent log lines, base64-encoded, in the same response as the invocation itself — no separate trip to CloudWatch needed for a quick check:
aws lambda invoke --function-name my-function out.json --log-type Tail --query 'LogResult' --output text --cli-binary-format raw-in-base64-out | base64 --decode
That single command surfaces the START, END, and REPORT lines for the run you just triggered, Max Memory Used included, in the time it takes to run it — handy when you're trying to reproduce a kill on demand instead of waiting for one to happen on its own in production.
The second is a CloudWatch Logs Insights query worth keeping in a notes file. Lambda automatically populates a set of fields on every log entry, including @maxMemoryUsed and @memorySize, so you can compare the two across dozens of invocations in a single query instead of opening log streams one at a time:
fields @timestamp, @maxMemoryUsed, @memorySize, @duration, @billedDuration | sort @maxMemoryUsed desc | limit 20
Sorting by @maxMemoryUsed descending puts your closest calls right at the top, which finds a slow memory creep far faster than scrolling through individual REPORT lines by eye. If you want to watch it happen live while you retest, CloudWatch Logs Live Tail streams new log events to the console as they arrive — it's a separate feature from the CLI's --log-type Tail option, billed by session-minute, so it's worth opening for a focused debugging session rather than leaving running.
Fix 3: /tmp storage filling up
Every Lambda function gets a scratch directory at /tmp that it can write to during execution — call it a temporary workbench where the function can leave files it needs partway through the job, the way you'd clear a corner of your kitchen counter for one recipe. By default that workbench is 512 MB, and it can be resized up to 10,240 MB per function. If your function downloads a file, extracts a zip archive, converts a video, or writes intermediate output there and the total exceeds the configured size, the write itself will typically fail with a disk-space error your code can catch — but on some runtimes and under memory pressure at the same time, the combination shows up in the logs as the same generic "signal: killed" instead of a clean "no space left on device."
Two things worth knowing that catch people out here: /tmp storage is shared across every invocation running inside the same warm environment, and it is not automatically wiped between invocations on a warm start — only when a fresh environment is created. A function that writes files to /tmp and never deletes them can fill that scratch space across dozens of warm invocations even though each individual invocation's file looks small.
- Delete what you write. At the end of the handler, remove any files your function created in
/tmpunless you deliberately want them to persist for the next warm invocation (some caching patterns do this on purpose — just do it on purpose). - Resize /tmp if the workload genuinely needs more room. In the console, that's the same General configuration screen as the memory setting — look for Ephemeral storage and set a value up to 10,240 MB. In infrastructure-as-code, it's the
EphemeralStorageproperty. - Check the cost trade-off before maxing it out. The first 512 MB of ephemeral storage is included at no extra charge; anything above that is billed per GB-second for the duration of the invocation, so a function resized to 10 GB "just in case" pays for that headroom on every single invocation, not only the ones that need it.
Fix 4: Lambda Extensions that miss their shutdown window
Ethan explains the shutdown handshake
Jake asked Ethan why a function that adds an observability agent — the kind that reports metrics to a monitoring dashboard — suddenly started getting killed a few times a day after months of running clean. "Think of a Lambda Extension as a second worker sharing the same shipping container as your main code," Ethan told him. "When Lambda decides to shut that container down for good, it doesn't yank the plug immediately. It sends a polite heads-up first — a SIGTERM signal — and gives everyone inside a short window to wrap up and leave on their own. Your runtime gets up to 500 milliseconds of that window if you're running an internal extension, or up to 300 milliseconds for an external one, and after that, Lambda stops being polite and sends SIGKILL to whatever's still standing."
If a registered extension — a logging agent, a custom telemetry shipper, anything hooked into the Extensions API — doesn't finish its own cleanup inside that short window, Lambda kills the whole environment, extension and function code together, and that's the "signal: killed" you see. The window is intentionally tight, because Lambda can't leave an environment lingering indefinitely on every single freeze-and-recycle cycle.
✅ Why this is worth handling properly
Catching SIGTERM in your own function code and using it to flush buffers, close database connections, or finish a partial upload is the difference between a clean shutdown and a corrupted half-written record every time Lambda recycles an environment. It costs a few lines of signal-handling code and prevents a class of bug that otherwise only shows up under load, exactly when you can least afford it.
If you maintain the extension yourself, the fix is to make its shutdown handler faster — flush asynchronously, skip anything non-essential during shutdown, and register a signal handler for SIGTERM rather than relying on the process to exit naturally. If it's a third-party extension (an observability vendor's agent, for instance), check that vendor's own release notes for known shutdown-timing issues before assuming your code is the culprit — this is one of the few cases in this whole list where the fix genuinely isn't in your own handler.
Fix 5: container image functions killed on cold start
If your function is packaged as a container image rather than a plain .zip deployment package, and the kill happens close to every cold start rather than intermittently mid-run, the cause is usually somewhere in the image, not in your handler logic at all. Two things to check first:
| Symptom during cold start | Likely cause |
|---|---|
| Init phase runs long, then killed, on every invocation | Heavy initialization (loading a large model, unzipping bundled assets) exceeding memory during init, before your handler even starts |
| Killed only on the architecture you just switched to (x86 vs Arm) | Base image built for the wrong architecture, or a native dependency compiled for the other one |
| Works locally with Docker, fails only on Lambda | Local Docker run had more memory available than the Lambda memory setting you configured |
The single highest-value check here is memory during the init phase specifically — a heavy container image (one that loads a machine learning model into memory before it can serve its first request, for example) can consume most of a function's memory budget just getting ready, leaving almost nothing for the actual invocation that follows. The REPORT line's Max Memory Used figure for that first, cold invocation will usually make this obvious once you know to look at it separately from a later warm invocation.
VPC-attached functions: a different kind of stall that looks the same
If your function connects to resources inside a VPC — a private RDS database, an internal API — and it's getting killed specifically on the calls that reach into the VPC, the underlying issue is usually a networking stall rather than memory at all: a security group not allowing outbound traffic on port 443, a subnet with no route to the internet when the function also needs to call a public AWS API, or the function's own timeout being set too aggressively for a cold VPC network interface to attach. These stalls tend to end in a plain timeout rather than a "killed" signal, but they're worth ruling out on the same pass, because the symptoms — "the function just stops responding" — get described to a support forum in nearly identical words.
Ethan's rule of thumb for Jake: "Avoid putting a function in a VPC at all unless it genuinely needs to reach something private. Every VPC attachment adds a network interface Lambda has to manage, and it's one more moving part that can be misconfigured. If the only reason you added a VPC was 'it felt more secure,' that's not a reason — public AWS service endpoints are already encrypted in transit."
What this actually costs Jake's shop
The photo-backup function that got killed mid-batch that afternoon wasn't just an annoyance — it was invoked asynchronously by an S3 upload event, and Lambda's default retry behavior for asynchronous invocations is to try the failed event again automatically before giving up. That's usually helpful. In this case it meant the same batch of photos got picked up, partially processed, killed, and retried three separate times before Jake even noticed anything was wrong, each retry burning invocation time on top of the customer's wait. A dead-letter queue or an on-failure destination would have caught the event after retries were exhausted and surfaced it immediately instead of letting it silently loop.
♂️ Jake's Reality Check
"If it retries automatically, isn't that basically the same as it working?"
No — because a retry on the same code with the same input hits the exact same wall. If the root cause is a memory ceiling or a connection leak, retrying doesn't route around it. It just repeats the failure, on a delay, while your customer's photos still aren't backed up.
Building an early warning system so this doesn't repeat
Lambda's basic, no-extra-cost monitoring doesn't include a memory-usage metric at all — that's a genuine gap, and it's why so many people only discover the ceiling after they've already hit it. To see memory trends before they turn into a kill, you need Lambda Insights turned on for the function, which reports a memory_utilization metric (memory used as a percentage of what's configured) into a CloudWatch namespace called LambdaInsights.
- Open the function in the Lambda console, go to the Configuration tab, and choose Monitoring and operations tools.
- Turn on Lambda Insights in the enhanced monitoring section — this attaches the Lambda Insights extension as a layer automatically.
- In CloudWatch, create an alarm on the
memory_utilizationmetric for that function, with a threshold around 80–85% rather than waiting for 100%. - Route that alarm to an SNS topic that emails or pages you, so you find out the function is approaching its ceiling on a quiet Tuesday afternoon, not from an angry customer message on a Saturday.
Lambda Insights isn't free — you pay both for the extension's own small overhead and for the CloudWatch metrics and logs it generates — so it's worth turning on for functions that matter in production rather than blanket-enabling it across every function in an account.
What raising memory actually costs, in real numbers
Jake's next question, reasonably, was whether "just raise the memory" was going to quietly blow up his AWS bill. It's worth working through the actual arithmetic instead of guessing, because the answer is usually smaller than people expect — and because a higher memory setting often finishes the same job faster, which claws back some of that extra cost on its own.
Lambda's on-demand pricing has two parts: a charge per request, and a charge per GB-second, where a GB-second is the memory allocated (in GB) multiplied by the execution duration (in seconds) multiplied by the number of invocations. In the US East (N. Virginia) Region, the duration price for the first pricing tier is $0.0000166667 per GB-second on x86, and the request price is $0.20 per one million requests, after a permanent free tier of 400,000 GB-seconds and one million requests every month.
| Memory setting | Avg. duration | Billable GB-seconds | Monthly compute charge |
|---|---|---|---|
| 512 MB | 1.2 s | 800,000 | ≈ $13.33 |
| 1,024 MB | 0.7 s | 1,000,000 | ≈ $16.67 |
Here's an illustrative example, built from those published rates rather than any measured function: say a job like the photo-backup function runs 2,000,000 times a month. At 512 MB and an average 1.2 seconds per run, that's 2,000,000 × 1.2 × 0.5 GB = 1,200,000 GB-seconds; minus the 400,000 free, 800,000 billable GB-seconds comes to about $13.33. If doubling the memory to 1,024 MB also speeds the same CPU-bound work up to 0.7 seconds — a realistic outcome, since extra memory buys proportionally more CPU — the math becomes 2,000,000 × 0.7 × 1 GB = 1,400,000 GB-seconds; minus the same free tier, 1,000,000 billable GB-seconds comes to about $16.67. Request charges add roughly $0.20 either way, since 2,000,000 requests minus the 1,000,000 free requests leaves 1,000,000 billable requests at $0.20 per million.
Doubling the memory, in this example, adds about $3.34 a month to the bill — nowhere near the order-of-magnitude jump people sometimes assume — while cutting the risk of a memory kill sharply and finishing every customer's job faster. Whether your own function sees the same speed-up depends entirely on whether it's actually CPU-bound; that's exactly what the Power Tuning tool mentioned earlier is built to measure instead of guessing.
Let AWS calculate the right number instead of guessing
If tuning memory by hand across a growing list of functions sounds tedious, AWS Compute Optimizer will do it for you automatically. Once you opt in — it's off by default, at the account or AWS Organizations level — it analyzes a function's utilization metrics over the trailing 14 days and produces a memory size recommendation, refreshed daily, right alongside the function's current cost and its projected cost at the recommended size.
It comes with real limits worth knowing before you rely on it: Compute Optimizer only generates a recommendation for functions with configured memory at or below 1,792 MB, only for functions invoked at least 50 times in the trailing 14 days, and only for x86_64 functions — Arm/Graviton functions aren't covered yet. A function above that memory threshold, a brand-new function with thin invocation history, or an Arm function shows as "Unavailable" or "Insufficient data" in the console instead of getting a recommendation. Once a function does qualify, you can view and accept the recommendation right from the same General configuration screen in the Lambda console where you'd change memory by hand.
Setting memory and timeout in code, so the fix survives the next deploy
Ethan's other habit, after fixing a function by hand in the console, is to immediately put the same value into the function's actual template. A console change made under pressure late at night is exactly the kind of change that gets silently overwritten the next time someone redeploys from an older template. If you manage the function with AWS SAM or CloudFormation, memory, timeout, and ephemeral storage all live on the function resource itself, right next to each other:
Resources: MyFunction: Type: AWS::Serverless::Function Properties: CodeUri: . Handler: app.handler Runtime: python3.13 MemorySize: 1024 Timeout: 120 EphemeralStorage: Size: 2048
Committing that change means the next teammate who redeploys the stack inherits the fix automatically, instead of quietly reverting it back to whatever the template said before — which is the single most common way a "fixed" memory kill quietly comes back to life a few weeks later.
The adjacent problem you'll hit next: explicit exit codes
Once you've ruled out a kill, you may still run into a close cousin: Runtime exited with error: exit status 129, or a similar numeric exit code instead of "killed." These come from a completely different place — your own code, or a library it calls, explicitly telling the process to stop, using something like process.exit() in Node.js, exit() or quit() in Python, os.Exit() in Go, or Environment.Exit() in .NET. Lambda expects your handler to return a value, not to terminate the process directly, so any of these calls — even ones buried three dependencies deep in a library you didn't write — end the whole execution environment abruptly. The fix is to search your codebase and dependencies for those calls and replace them with a normal return or a thrown exception your handler can catch, rather than a process-level exit.
| What the log says | What triggered it | Who's in control of the fix |
|---|---|---|
| "Task timed out after X.XX seconds" | Function's timeout setting reached before it finished | You — raise timeout or speed up the code |
| "signal: killed" | Lambda's own supervisor forcibly ended the process (memory, fd, /tmp, extension) | You, mostly — see the fixes above |
| "exit status 129" (or another explicit number) | Your code or a dependency called a process-exit function directly | You — remove the explicit exit call |
It's worth learning this distinction once and keeping it handy, because "timed out" and "killed" get used interchangeably in casual conversation and support tickets, but they point at completely different fixes — one is about giving the function more time, the other is about giving it more resources or fixing a leak, and reaching for the wrong one wastes a deploy cycle.
Third-party tools worth knowing about — and when to skip them
Beyond AWS's own tools, a handful of open-source and third-party observability platforms plug into Lambda through the same Extensions mechanism to give you dashboards on top of the raw metrics. They're genuinely useful once you're running dozens of functions and can't eyeball each one's REPORT line by hand. For a single function that fails a few times a week, though, they're overkill — the free REPORT line plus a CloudWatch alarm on Errors and Throttles covers the same ground for zero extra cost, and adding another extension is one more thing that itself needs to shut down cleanly (see the extension section above) before it becomes part of the problem rather than the solution.
A five-minute prevention checklist
- Move database and HTTP client connections outside the handler, to the top level of the file, so warm invocations reuse them instead of opening new ones.
- Delete anything your function writes to /tmp before the handler returns, unless you're deliberately caching it for the next warm invocation.
- Set an appropriately generous, but not unlimited, timeout — a timeout close to your average duration risks unexpected cutoffs, while an unnecessarily long one just delays your discovery of a genuine hang.
- Turn on Lambda Insights on production functions and alarm on memory_utilization before it hits 100%.
- Configure a dead-letter queue or an on-failure destination for anything invoked asynchronously, so a killed invocation surfaces instead of silently retrying into the void.
When nothing above fixes it
If the function is already at the 10,240 MB memory ceiling, /tmp is resized generously and cleaned up properly, connections are reused correctly, there's no extension involved, and it's still getting killed — the honest next step is to accept that the workload may not be a good fit for a single Lambda invocation at all. AWS Lambda functions cap out at 900 seconds (15 minutes) of runtime no matter what, and that ceiling can't be raised by any support request, unlike some of the soft limits elsewhere in the service. Workloads that genuinely need more time or a bigger memory footprint than Lambda's hard ceilings allow are usually better split across multiple, smaller Lambda invocations coordinated by AWS Step Functions, or moved to a longer-running compute option like AWS Fargate or an EC2-based worker.
If you don't have visibility into what's actually running inside the container image — say, you inherited the function from someone who's since left, or it's built on a base image with dependencies nobody fully documented — that's also a real limit worth naming plainly: you cannot reliably fix a kill you can't attribute to a specific cause, and guessing your way through memory bumps indefinitely isn't a strategy, it's an expensive habit.
Frequently asked questions
What does "Runtime exited with error: signal: killed" mean in AWS Lambda?
It means the Lambda execution environment forcibly terminated your function's process with a SIGKILL signal from the outside, rather than your code returning an error or throwing an exception on its own. The most common trigger is the process using more memory than the function's configured Memory setting allows.
Is "signal: killed" the same thing as a Lambda timeout?
No. A timeout shows up as "Task timed out after X.XX seconds" and means the function was still running when the configured Timeout was reached. "Signal: killed" means the process was actively terminated for a different reason, most often exceeding its memory limit, and can happen well before the timeout is ever reached.
Why did my Lambda function get killed even though I already raised the memory?
Memory isn't the only hard ceiling in a Lambda execution environment. If the real cause is a leaked database connection, an open file handle, or too many threads, you'll eventually hit the fixed limit of 1,024 file descriptors and threads regardless of how much memory you've configured, since that limit doesn't scale with the memory setting.
Does increasing Lambda memory always fix "signal: killed"?
No, but it resolves it in the majority of cases, because true out-of-memory kills are the most common cause. Always confirm by comparing the REPORT line's "Max Memory Used" to the configured "Memory Size" before assuming a memory increase is the fix — if the two numbers aren't close, look at connection reuse, /tmp usage, or extensions instead.
How do I check how much memory my Lambda function actually used?
Open CloudWatch Logs for the function, find the log stream containing the failed invocation, and look at the line beginning with "REPORT RequestId:". It lists "Memory Size" (what's configured) next to "Max Memory Used" (what the invocation consumed) for that specific run.
Can too many database connections cause "signal: killed"?
Yes. If a function opens a new database connection inside the handler on every invocation instead of reusing one created outside it, warm execution environments accumulate open connections across invocations until they hit the file descriptor ceiling, and the environment gets killed with no memory warning at all.
What is the file descriptor limit in AWS Lambda?
Every Lambda execution environment has a combined limit of 1,024 open file descriptors and threads. This ceiling is fixed and does not increase with a higher memory or CPU allocation.
Can filling up /tmp storage cause a Lambda function to be killed?
Filling /tmp usually surfaces as a distinct "no space left on device" error your code can catch, but under combined memory and disk pressure it can present as the same generic "signal: killed" message. By default /tmp is 512 MB and can be resized up to 10,240 MB, and it persists across warm invocations in the same environment, so files left uncleaned can accumulate.
Why does my Lambda function fail only on the first (cold start) invocation?
A container-image function that gets killed specifically on cold starts is usually consuming too much memory during its initialization phase — loading a large model or unzipping bundled assets before your handler code ever runs — leaving little memory headroom for the actual first invocation.
Do Lambda Extensions cause "signal: killed" errors?
They can, if they don't finish their own shutdown work within the short window Lambda gives them after sending a SIGTERM signal — up to 500 milliseconds for an internal extension, 300 milliseconds for an external one. If the extension is still running when that window closes, Lambda sends SIGKILL to the whole environment.
Can a container image cause "Runtime exited with error: signal: killed"?
Yes, most often through heavy initialization work that exceeds available memory before the handler starts, or through an architecture mismatch between the base image and the Lambda function's configured architecture. Check the "Max Memory Used" figure on the first, cold invocation specifically, separate from later warm invocations.
Does a VPC-attached Lambda function have anything to do with this error?
VPC networking issues — a misconfigured security group or a subnet with no internet route — more commonly produce a plain timeout than a "killed" signal, but they're worth ruling out on the same troubleshooting pass since both can look like "the function just stops."
How do I get alerted before my Lambda function runs out of memory again?
Turn on Lambda Insights for the function under Configuration > Monitoring and operations tools, then create a CloudWatch alarm on the memory_utilization metric with a threshold around 80–85%, routed to an SNS topic so you're notified before the function actually gets killed rather than after.
What's the maximum memory I can give a Lambda function?
10,240 MB. Memory can be configured anywhere from 128 MB up to that ceiling, in 1-MB increments, through the console's General configuration screen or the update-function-configuration API.
Will retries make "signal: killed" go away on their own?
No. Asynchronous invocations retry automatically on failure, but a retry runs the same code against the same underlying cause — if that cause is a memory ceiling or a connection leak, the retry hits the identical wall and fails again, just later.
Is there a way to see the exact exit code instead of just "killed"?
"Signal: killed" specifically means the process was terminated by a signal rather than exiting with its own numeric status code, so there isn't a separate exit code to find for a true kill. If you instead see a message like "exit status 129," that's a different situation — your own code or a dependency called an explicit process-exit function, and the number itself is a standard Unix exit code you can look up.
Revision note. Written September 2026, Lambda's quotas and rates move slowly but do change over time, so if you're reading this well after 2026, double-check the memory ceiling and GB-second pricing on AWS's own pages before relying on the exact numbers here. If you've been staring at a killed function with no other clues for the last hour, you're not missing something obvious — this error genuinely hides its cause on purpose, and working through it in order is the fastest way out, not a sign you did something wrong.