Lambda "Process Exited Before Completing Request": Fixes
"Process exited before completing request", logged by AWS Lambda as Runtime.ExitError, means the program running your function stopped before it handed its answer back. Lambda is not telling you why. The why is one line above, in the CloudWatch log for that request, and it is almost always one of five things: a process.exit() or os.Exit() or sys.exit() left in the code, a native library built for the wrong processor (x86_64 vs arm64), a layer or extension that crashed, memory, or an initialization failure that never reached your code. The surprise most people meet on this page: the look-alike bug, a Node.js handler that returns before an await finishes, produces no error at all. The customer's email simply never arrives, and the log says success.
Jake runs a phone repair shop. His booking page is a form; behind the form is a small Lambda function that Ethan, his friend who does the technical side, wrote last year. It saves the booking and emails the customer a confirmation. On Tuesday a customer booked a screen repair, got no email, and rang the shop. Jake opened the AWS console for the first time in months and found a red line he could not read.
Jake: "It says 'Process exited before completing request.' Which process? I don't run any processes, I run a shop."
Ethan: "You do now. Every time someone presses Book, AWS starts a small program, your program, hands it the booking, and waits for an answer. This line says the program stopped talking before it answered. That is all it says."
Jake: "So AWS is broken?"
Ethan: "AWS is the one reporting it. Think of the customer who says 'my phone just died.' That is the complaint, not the diagnosis. We are going to read the line above the complaint."
If "Lambda", "runtime" and "handler" are new words, our plain-English page on what AWS Lambda is takes five minutes and makes the rest of this one easy. Jake had not read it either. He did fine.
What the line actually says, and the four errors it is not
Three things take part in every Lambda invocation, and the error names one of them. The invocation is the request: "here is a booking, deal with it." The handler is your code, the JavaScript, Python or Go function that deals with it. The runtime is the program that loads your handler, hands it the event, and carries the answer back to Lambda. "Process exited before completing request" means the runtime process, or an extension running beside it, stopped before that carrying-back happened. Lambda files it under the error type Runtime.ExitError. Older log formats print the same event as Runtime exited without providing a reason; newer ones put Status: error Error Type: Runtime.ExitError on the REPORT line. Same event, two spellings.
Jake: "Runtime, handler, invocation. Give me the shop version."
Ethan: "A customer walks in with a phone: that's the invocation. You are the handler; you fix the phone. The runtime is the counter staff who take the phone from the customer, bring it to you, and carry it back with the receipt. This error says the counter staff walked out mid-shift. Maybe you told them to. Maybe they fainted. The customer only knows nobody came back."
That matters because four other failures look identical from the customer's side and need completely different repairs. Learn to tell them apart before touching a setting:
| What the log says | What really happened | First thing to check |
|---|---|---|
Runtime.ExitError / "Process exited before completing request" | The runtime or an extension stopped unexpectedly | The REPORT line and the lines just above it |
Runtime.ImportModuleError | Your code could not be loaded at all | Handler name, zip layout, missing dependency |
| "Task timed out after N seconds" | Your code ran but never finished in time | Duration in REPORT, slow external calls |
| Success, but the email never arrived | The handler returned before an unawaited task finished | Every await in the handler |
Runtime.InvalidEntrypoint | The executable or container entrypoint is wrong | Where bootstrap sits, image CMD |
Those are not interchangeable. More memory can fix a real memory shortage and cannot fix a process.exit(1) somebody typed. Reinstalling a package can fix a wrong-architecture binary and cannot make an unawaited promise part of the answer. Every step on this page answers a specific question, and the first question is: which of these five am I looking at?
The first ten minutes: find one failed request and keep it
If the function has failed more than once, pick one failure and stay with it. In the console: Lambda → Functions → your function → Monitor → View CloudWatch logs. The log group is /aws/lambda/<function-name> unless somebody changed it. Open the log stream from the time the customer booked, find the request, and copy two things: the request ID and the timestamp. The request ID ties together the START, your own messages, END and REPORT lines of one invocation, and it keeps you from mixing the original failure up with the retry that followed it.
From a terminal, the same thing is three commands. The first one is not optional, because a console open on the production account and a terminal pointed at a staging account is the classic way to "prove" that a fix did nothing:
aws sts get-caller-identity
aws logs tail /aws/lambda/my-function --since 30m --region us-east-1
aws logs tail /aws/lambda/my-function --since 30m --follow --region us-east-1
If the log group is empty, that is a separate problem and usually a permissions one: the function's execution role needs to be allowed to create log groups and streams and to write events. Our empty-log-group fix covers it. Missing logs hide the evidence; they are rarely the reason the process died.
Jake: "Why do I need a request ID? There was one customer."
Ethan: "There were at least two attempts, because AWS often retries a failed one by itself. Two attempts, two stories, same minute. The request ID is the job ticket number. Without it you will read the retry's log and swear the first one never happened."
Jake: "Fine. Where do I write it?"
Ethan: "On the same note as the Region, the function name, the alias, and the time. Four things. We do not touch a setting until those four are written down."
Reading the REPORT line like a till receipt
Every invocation ends with a REPORT line. It is a receipt, not a diagnosis, and the mistake is to read it alone. Here is what Jake's looked like, with a made-up request ID:
START RequestId: example-123 Version: $LATEST
2026-10-09T09:14:01Z example-123 Starting order 4521
END RequestId: example-123
REPORT RequestId: example-123 Duration: 412.00 ms Billed Duration: 413 ms Memory Size: 256 MB Max Memory Used: 98 MB Status: error Error Type: Runtime.ExitError
Read it as four facts and one pointer. Duration is how long the code ran; Billed Duration is what you paid for; Memory Size is what you configured; Max Memory Used is the peak the process reached. Status and Error Type name the outcome. The pointer is everything above the REPORT: the last line your own code printed is the closest you will get to the scene of the crash.
| What you see | What it suggests | Where to look next |
|---|---|---|
| Status: error, Runtime.ExitError, your last log line is "Loading image library" | A native dependency crashed on load | The native-module section |
| Your last line is "Rejecting invalid order", then the exit | Code called process.exit / sys.exit / os.Exit | The runtime sections below |
| No line from your handler at all, very short duration | Initialization, imports, a layer or an extension failed before your code ran | Layers and extensions; look for an INIT_REPORT |
| Max Memory Used close to Memory Size | Memory pressure is plausible | Payload size and allocations |
| Duration close to the timeout | You are looking at a timeout, not an exit | External calls, loops |
| Status fine, and still no email | The handler returned before the work finished | The Node.js section |
Sometimes there is an INIT_REPORT near the failure. That belongs to a different phase, the one where Lambda loads your code before any booking arrives. If the INIT_REPORT is the one carrying the error, your handler never ran, and the suspects are imports, layers, extensions and entrypoints, not the business logic.
Jake: "Mine says 98 MB used out of 256. So it's not memory. Done?"
Ethan: "It makes memory an unlikely suspect, which is useful. It does not name the real one. A peak memory number cannot explain a program that chose to quit."
Jake: "Chose to quit?"
Ethan: "We are about to find out. Your last log line before the crash was 'Starting order 4521.' Nothing after it. That means the crash happened inside the order code, after it started and before it printed anything else. That narrows it to about forty lines."
Which runtime, which processor: the one command before you rebuild anything
Before you rebuild a single file, know what is actually deployed. The console shows it on the function's configuration page; the CLI gives it in one line:
aws lambda get-function-configuration --function-name my-function --region us-east-1 \
--query '{Runtime:Runtime,Architectures:Architectures,Memory:MemorySize,Timeout:Timeout,PackageType:PackageType,Layers:Layers[*].Arn,State:State,LastUpdateStatus:LastUpdateStatus}'
Two surprises hide here regularly. The laptop that built the package is an ARM machine and the function is x86_64, or the other way round. And the staging function is still on Node.js 20 while production moved to Node.js 22 last month, so the package that worked yesterday is being run by a different engine today. Write down the runtime and the architecture next to the request ID. Everything below branches on them.
🙋 Jake's Reality Check
Jake: "Could I just switch it to a different runtime until it starts working?"
Ethan: "You could, and then you would have two mysteries instead of one. Match the package to the runtime that is there, find the one thing that is wrong, fix it. Upgrade on a quiet day, as its own change."
Node.js: the handler that finished before the email left
This is the look-alike, and it is the most common reason a confirmation never arrives, so it comes first even though it does not usually produce Runtime.ExitError at all. In JavaScript, an async function returns a promise, and Lambda waits for that promise. Anything you started but did not await is not part of the promise. The handler returns, Lambda says "done", and the email is still in flight when the lights go out.
export const handler = async (event) => {
sendNotification(event.orderId); // started, never awaited
return { statusCode: 200, body: "Accepted" };
};
The browser shows success. The log shows success. The customer gets nothing. The fix is one word:
export const handler = async (event) => {
const result = await sendNotification(event.orderId);
return { statusCode: 200, body: JSON.stringify(result) };
};
Now the answer depends on the send. If the send fails, the handler fails, visibly, and whatever invoked it can retry. Wrap it in try/catch if you want a readable log line, then throw the error on; swallowing it turns a visible failure back into a silent one. When two pieces of work are independent, Promise.all waits for both. When one depends on the other, save the booking first and only then send the email, because the email should never say "booked" about a booking that was never written:
export const handler = async (event) => {
try {
const saved = await saveOrder(event);
const notified = await sendNotification(saved.orderId);
return { saved, notified };
} catch (error) {
console.error("Order workflow failed", error);
throw error;
}
};
Node.js 20 and 22 still support the older callback style, and context.callbackWaitsForEmptyEventLoop belongs to that style. It is not a switch that fixes an async handler; if you use async/await, keep the work inside the returned promise and leave that setting alone.
Jake: "Hang on. You said the error was the program quitting. This is the program finishing too early. Which one do I have?"
Ethan: "Possibly both, which is why I showed you this first. Your log had the ExitError, so something did quit. But when we fix that, the email still has to actually be sent before the handler says it was. Otherwise we will fix the crash and the customer still gets nothing, and you will tell me the fix did not work."
Jake: "Return when the order is finished, not when it has started."
Ethan: "That is the whole section."
process.exit: hanging up on Lambda mid-sentence
Now the real exit. Search the code, and anything it imports at startup, for process.exit(. This call ends the Node.js process. It does not "return an error" and process.exit(0) does not mean "return success". It means the counter staff put the phone down and walked out. Jake's function had exactly this, written in a hurry a year ago:
export const handler = async (event) => {
if (!event.orderId) {
console.error("Missing order ID");
process.exit(1);
}
return { ok: true };
};
The customer's booking arrived without an order ID because of a change on the form, the code hit this branch, and the process ended. Replace the exit with an ordinary error. Throw when the event is invalid, or return a clearly shaped validation response if the thing calling you expects one:
export const handler = async (event) => {
if (!event.orderId) {
throw new Error("Missing orderId");
}
return { ok: true, orderId: event.orderId };
};
Why this is better even though both outcomes are "the booking failed": a thrown error travels back through Lambda's normal channel, gets logged with its message, and can be retried or routed to a failure destination. A process exit cuts that channel. You get the generic line Jake saw instead of "Missing orderId", and you spend a Tuesday evening finding out what it meant. Log the request ID and a non-sensitive order identifier with the error, not the whole payload; CloudWatch is not the place for a copy of your customer list.
Jake: "Couldn't I catch every error and return something like {success: false}? Then nothing ever crashes."
Ethan: "Only if the thing calling you knows what that means. If the booking form treats any answer as success, you have swapped a crash for a silent lie, and the customer still gets no email. A failure should look like a failure to whoever needs to act on it."
Python: sys.exit, SystemExit, and the thread nobody waited for
Python has the same two bugs with different names. The first is sys.exit() in validation code. In Python, sys.exit raises SystemExit; if nothing catches it, the interpreter is on its way out, and what Lambda reports depends on how far it got. Replace it with an exception, or with a deliberate validation response:
import sys
def lambda_handler(event, context):
if not event.get("orderId"):
sys.exit(1) # the bug
return {"ok": True}
def lambda_handler(event, context):
order_id = event.get("orderId")
if not order_id:
raise ValueError("Missing orderId") # the fix
return {"ok": True, "orderId": order_id}
The second is the Python twin of the unawaited promise: a worker thread. An uncaught exception inside a threading.Thread kills that thread and prints a traceback; it does not kill the interpreter, and it does not stop the handler from returning {"ok": True}. If the thread was sending the email, the email is gone and the log says success.
import threading
def send_notification(order_id):
raise RuntimeError("Notification service failed")
def lambda_handler(event, context):
worker = threading.Thread(target=send_notification, args=(event["orderId"],))
worker.start()
return {"ok": True} # returns before the worker fails, and never hears about it
A plain join() waits for the thread but does not re-raise its exception in your handler. A future does both, which is why it is the right tool when the work matters:
from concurrent.futures import ThreadPoolExecutor
def send_notification(order_id):
if not order_id:
raise ValueError("Missing orderId")
return {"sent": True, "orderId": order_id}
def lambda_handler(event, context):
with ThreadPoolExecutor(max_workers=2) as pool:
future = pool.submit(send_notification, event.get("orderId"))
result = future.result() # waits, and re-raises the worker's exception here
return result
One more Python case belongs to the next section rather than this one. If the log shows a native crash, a segmentation fault or a shared-library message instead of a Python traceback, no amount of try/except will help, because the thing that died was not Python. That is a packaging problem.
Jake: "Why would anyone put the email in a separate thread in the first place?"
Ethan: "To make the page feel faster. The form gets its answer while the email is still being sent. It is a fine idea on a server that stays running. Lambda does not stay running; it stops the moment you answer. The thread is a technician who starts a repair after the shop has closed."
Go: os.Exit, the file called bootstrap, and the wrong chip
Go functions have the same exit bug, spelled os.Exit(), and a packaging rule the other runtimes do not. First the exit. In a handler, os.Exit ends the whole executable immediately, before the Lambda runtime library can report anything. Return an error instead; the library knows what to do with it:
// the bug
func handler(ctx context.Context) error {
os.Exit(1)
return nil
}
// the fix
func handler(ctx context.Context) error {
return errors.New("order processing failed")
}
func main() { lambda.Start(handler) }
Now the rule. On the OS-only runtimes (provided.al2023 is the documented choice for new functions) your compiled program must be a file called bootstrap at the root of the zip, built for Linux and for the processor the function uses. Not for your laptop:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags lambda.norpc -o bootstrap main.go
zip function.zip bootstrap
# for an arm64 function:
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -tags lambda.norpc -o bootstrap main.go
CGO_ENABLED=0 is right for a program with no C dependencies. If yours needs C libraries, you need a Linux build environment matching the target, not a flag. A binary for the wrong target is a packaging mistake; raising the timeout will not make it start. If you are moving an old function to provided.al2023, do it behind a staging alias, because the runtime change brings its own library-compatibility questions.
Jake: "Why does Go get a special chip warning when you said Node and Python need one too?"
Ethan: "Because with Go you ship the whole engine yourself. If the engine is built for the wrong car, nothing of yours ever runs. With Node and Python the engine is AWS's, and only the spare parts you bolted on can be wrong. Which is the next section."
Native modules: the spare part that fits Linux but not this engine
If the function works on the laptop and dies right after importing a package, suspect a native module. That is code compiled for a specific processor and operating system rather than interpreted: image libraries, number-crunching packages, database drivers, anything that ships a .so file. Lambda runs Linux on either x86_64 or arm64. A library built for one does not load on the other, and a library built on macOS or Windows does not load on either.
| Function architecture | What the package contains | Verdict |
|---|---|---|
| x86_64 | Linux x86_64 binary | Matches; still check the runtime version |
| arm64 | Linux aarch64 binary | Matches; still check the runtime version |
| arm64 | Linux x86_64 binary | Wrong processor |
| x86_64 | Linux aarch64 binary | Wrong processor |
| Either | macOS or Windows build | Wrong operating system |
For Node.js, install for the target explicitly. The image library sharp is the classic example:
mkdir -p nodejs
npm install --prefix nodejs --platform=linux --arch=x64 sharp # --arch=arm64 for an arm64 function
zip -r node-layer.zip nodejs/
For Python, ask pip for Linux wheels built for the function's interpreter, and refuse source packages so it cannot quietly compile something for your laptop:
pip install --platform manylinux2014_x86_64 --target=package --implementation cp \
--python-version 3.12 --only-binary=:all: --upgrade numpy
# arm64 function: manylinux2014_aarch64 instead
Match --python-version to the function's actual interpreter; a wheel for the right processor and the wrong Python version fails just as surely. If a package has no prebuilt wheel, build it inside a Linux container that matches the runtime. Copying a wheel that "worked on the other machine" is how Jake's image library got there in the first place.
Jake: "So a part can say Linux on the box and still be wrong."
Ethan: "A layer or a module is a spare part. The label says it fits Linux. You still need the right model of engine, and the right year. Processor, operating system, runtime version, all three."
Layers and extensions: five slots, 250 MB, and a list you must copy whole
A layer is a zip of shared code or libraries that Lambda unpacks into /opt beside your function. It has versions, and a function keeps using the version it was given even after you publish a newer one. If the failure started the day you added or changed a layer, start there.
Layout matters. A Python layer puts its packages under a top-level python/ folder; a Node.js layer uses nodejs/node_modules/, or a runtime-specific nodejs/node20/node_modules/ and nodejs/node22/node_modules/. A zip nested one folder too deep is not a layer, it is a zip. Standard zip-based functions can attach up to five layers, and the unzipped function plus layers must stay under 250 MB. Container-image functions do not use layers at all; their dependencies live in the image.
aws lambda get-function-configuration --function-name my-function --region us-east-1 --query 'Layers[*].Arn'
aws lambda get-layer-version --layer-name my-dependencies --version-number 3 --region us-east-1
The version ARN ends in a number; the layer's declared compatible runtimes and architectures are a hint, not a guarantee that every binary inside was built right. One trap when you change layers from the CLI: update-function-configuration --layers replaces the whole list. Pass every layer you want to keep, or you will remove working dependencies while fixing a broken one.
Extensions are the other tenant in that environment: monitoring and security agents that run beside the runtime and take part in the invocation lifecycle. They have their own binaries and their own compatibility needs, and an extension that crashes produces the same Runtime.ExitError your handler would. If your handler's code is clean and the last log lines belong to an extension, look at its version before rewriting the business logic.
Jake: "Can I just delete all the layers and start again?"
Ethan: "That might make the error vanish, and it might remove the code your function needs to run at all. Take one layer off at a time, on a copy, and keep the production version where it is until you know which one it was."
Memory: the number everyone raises first and should check last
If the process disappears during a big image or a large payload, memory is a fair suspect, and the REPORT line already gave you the first test: compare Max Memory Used with Memory Size. Close together, pursue it. Far apart, as Jake's 98 of 256 was, stop pursuing it until the other suspects are cleared.
Standard Lambda memory runs from 128 MB to 10,240 MB in 1 MB steps, and CPU scales with it. Separately, Lambda Managed Instances gained configurations up to 32 GB and 16 vCPUs in March 2026; that is a different offering with its own pricing, not a bigger slider on an ordinary function. Change standard memory in Configuration → General configuration → Edit, or:
aws lambda update-function-configuration --function-name my-function --memory-size 512 --region us-east-1
If memory really was the cause, the better fix is usually in the code: stream or chunk a large object instead of loading it whole. If the error continues while usage stays far below the ceiling, you were right to be suspicious, and the cause is one of the exits, binaries or extensions above. Our Lambda out-of-memory page goes deeper on sizing when memory is the real story.
Timeouts, import errors and entrypoints: when the next failure is a different failure
Fix the exit and the function may fail again with different words. That is progress, not proof the first fix was wrong, so read the new words. A timeout names the time limit; the cure is in slow network calls, DNS, loops and missing client timeouts, not in a bigger limit unless the work genuinely needs the time. An import error means the handler string, the zip layout or a dependency is wrong; our HandlerNotFound page is that whole story. An entrypoint error on an OS-only runtime means bootstrap is not at the root, or, on a container image, the image's command is wrong; do not apply zip instructions to an image.
One more distinction saves an argument: a synchronous Invoke API call can succeed as an API call while the function reports an error in its response. The CLI gives you both halves, the metadata and the body, and you need both to know what happened.
Jake: "Two customers complained. One booking took forever and failed, the other failed instantly."
Ethan: "Same complaint, two different faults. Instant death right after loading the image module is the wrong-chip problem. Forever-then-failure is a timeout, probably the email service not answering. Fix the package in one and the stalled call in the other, and do not let the fact that both customers said 'it failed' convince you it is one bug."
Versions and aliases: the green test that fooled Jake
Jake fixed the process.exit, pressed Test in the console, saw green, and the next booking failed anyway. The console tests $LATEST, the unpublished working copy. The booking form calls an alias, a name like production that points at a published version, a frozen snapshot. Until you publish a new version and move the alias, your customers run the old code.
Test the thing the form actually calls, with an event that reaches the failing branch, not an empty placeholder that skips it:
aws lambda invoke --function-name my-function --qualifier production \
--invocation-type RequestResponse --cli-binary-format raw-in-base64-out \
--payload '{"orderId":"4521"}' --region us-east-1 response.json
Check FunctionError in the metadata and read response.json; a successful API status is not a successful function. Confirm the account and Region too: aws sts get-caller-identity, the --region flag, and the ARN wired into the booking form. And never replay a real customer's event against production to "see if it works" unless you are certain it cannot charge, email or book them twice. A sanitized test event, or a copy of the function, costs nothing.
Jake: "So the test was green because I tested the version nobody uses."
Ethan: "The code you changed is not necessarily the code your customers run. Publish, move the alias, test the alias. Three steps, every time."
Retries, duplicate bookings, and what the crash actually cost
Here is the part that matters to the shop more than the code. If the function crashed after writing the booking and before replying, whatever invoked it may try again, and the retry can write a second booking or send a second email. Asynchronous invocations are retried up to two more times by default; synchronous callers decide for themselves; queues and streams have their own redrive and visibility rules. Find out which one your booking form uses before you assume anything about what the customer received.
The protection is idempotency: the same event, processed twice, produces one booking. For Jake the key is the order ID. Record that an order was completed, and treat a second delivery of the same order as a replay, not a new sale.
| Situation | What can happen | Guardrail |
|---|---|---|
| Exit before the booking is written | Error, possible retry, no damage | Keep the request ID, fix the cause |
| Exit after the booking is written | Retry duplicates the booking or the email | Idempotent order ID |
| Asynchronous invocation fails | Two automatic retries by default | On-failure destination, retry setting |
| Queue-triggered failure | Message becomes visible again | Visibility timeout, redrive policy |
| Stream-triggered batch fails | The batch is processed again | Batch failure reporting |
The bill, for perspective. A 256 MB function running 400 milliseconds, 100 times, consumes about 100 × 0.25 GB × 0.4 s = 10 GB-seconds. If every one of those was retried twice, that becomes 30 GB-seconds and 300 requests. Multiply by your Region's rates to get dollars; at Lambda's prices the crash cost Jake cents. The duplicate emails cost him a customer's trust, which is why this section exists.
Jake: "So after I fix the code, can I just re-run every failed booking from Tuesday?"
Ethan: "Only after you check what each one already did. A crash may have happened after the customer was charged. Replaying it would charge them again. Look for what the first attempt completed before you replay anything."
When none of it works: the smallest possible function
If you removed the exits, awaited the work, rebuilt the binaries and the error is still there, stop changing several things at once. Deploy a function that does nothing, with the same runtime and architecture as the broken one:
export const handler = async (event, context) => {
console.log("Diagnostic start", context.awsRequestId);
return { ok: true };
};
Deploy it as a separate function, never as a silent replacement of the real one. Then add things back in this order, invoking after each:
- Nothing. If this fails, the problem is the runtime configuration, the packaging, or an extension, and no code change will fix it.
- Your imports, one family at a time. When it dies on the image library, you have found your native module.
- Your layers, one at a time, keeping the rest of the list intact.
- Your extensions. If removing one changes the behavior, its version or compatibility is the story.
- Your external calls. If it dies here, you are in timeout territory, not exit territory.
For zip functions, list the archive and confirm the handler file or bootstrap is at the top level. For container images, compare the deployed image digest with the one you built; a tag can move, a digest cannot, and "yesterday's image worked" usually means yesterday's tag pointed somewhere else.
Jake: "The empty function worked. So AWS is fine and it's all my fault?"
Ethan: "It proves the simplest path runs in that configuration. It says nothing about your image library or your real booking event. It is not a verdict. It is the first rung of a ladder, and you climb it one import at a time."
What to write down, for support or for yourself in six months
If you get stuck, or if you have a support plan and want to use it, package the facts so someone else does not have to repeat your week. In this order:
- Account, Region, the full function ARN, runtime, architecture, package type (zip or image), and which alias or version the caller uses.
- One representative request ID, the time window, and the log stream around it, with the exact error spelling and whether it happened during initialization or invocation.
- The REPORT fields, plus any extension name, native library path, signal message or traceback that appeared.
- What changed recently: a runtime move from Node.js 20 to 22, a package rebuilt for a different chip, a new layer version, an image rebuilt under the same tag.
- Which isolation steps changed the outcome: "minimal handler runs; failure begins when the image dependency is imported" is worth more than "Lambda randomly crashes."
Keep tokens, secrets and customer details out of it. Support can look at the Lambda service side; your handler logic and your third-party binaries remain yours to fix, so this record is for your own engineer as much as anyone.
Jake: "I have nine screenshots and a headache."
Ethan: "Turn them into one timeline: the ARN, one request ID, what the customer did, and the last three things anyone changed. That is an incident report a stranger can use. The screenshots are a scrapbook."
Not meeting this error again at 9 a.m. on a Tuesday
Once it is fixed, protect the fix. Pin dependencies. Build native modules in a Linux environment that matches the function, every time, from a script, not by hand on whoever's laptop is nearest. Treat a runtime upgrade as a change that needs a matching build, not a dropdown. Keep versions and aliases explicit and promote one built artifact through staging to production instead of rebuilding it in each place. Record layer and extension versions in your deployment configuration so nothing drifts.
Then watch three numbers in CloudWatch: errors, duration and memory. Give every external call a timeout. If the booking confirmation is essential, await it, or hand it deliberately to a queue or a workflow service; never launch it and hope. And add one smoke test to the deployment that imports a real dependency and exercises the real response shape. The empty "hello world" handler passes while the image library is broken; one green test does not prove every path is healthy, so run the unusual ones too.
Jake: "What does 'fixed' even mean here? No red lines?"
Ethan: "No. A booking either completes with the record saved and the email sent, or it fails in a way your staff can see and act on, without charging or emailing the customer twice. That is the goal. 'No red lines' is just a quiet log."
The exact Runtime.ExitError questions people search for
What does "Lambda process exited before completing request" mean?
The Lambda runtime, or an extension running beside it, stopped before the invocation returned its response. Lambda reports it as Runtime.ExitError. Read the failed request's REPORT line and the log lines just above it before changing any setting.
What is Runtime.ExitError in AWS Lambda?
The error type Lambda assigns when a runtime or extension exits unexpectedly. It classifies the failure; it does not name the line of code responsible. Older logs print the same event as "Runtime exited without providing a reason."
Why did my Lambda function exit without a response?
Usually an explicit exit call (process.exit, sys.exit, os.Exit), a native library built for the wrong architecture, a crashed layer or extension, memory exhaustion, or an initialization failure. The last line your code printed before the REPORT points at which.
How do I fix "lambda exited with error"?
Find one failed request ID in CloudWatch, read its REPORT, check for exit calls in the code, confirm the runtime and architecture with get-function-configuration, and rebuild native modules for that target. Change the specific cause rather than raising memory and timeout blindly.
Does process.exit(0) cause Runtime.ExitError?
It can. Exit code zero still terminates the Node.js process instead of completing the handler response. Return a value for success and throw for failure.
Can a Node.js 20 or 22 Lambda return before async work finishes?
Yes. Any promise the handler does not await is not part of the result, so Lambda can finish the invocation while that work is still running. The log shows success and the side effect, such as an email, is lost. This is a missing-await bug, not a runtime exit.
How should I replace process.exit(1) in a Lambda handler?
Throw an error when the invocation should fail, or return a clearly shaped validation response when the caller expects one. Keep process termination out of handler code entirely.
Does Python sys.exit always produce Runtime.ExitError?
No. sys.exit raises SystemExit, and how Lambda reports it depends on how far the interpreter gets. Replace it with a raised exception or an intentional response either way.
Can an uncaught exception in a Python thread kill Lambda?
An ordinary exception in a threading.Thread ends that thread, not the interpreter, so the handler can return success while the thread's work was lost. Use a ThreadPoolExecutor future and call result() so the failure propagates into the handler.
How do I fix os.Exit in a Go Lambda function?
Return an error from the handler instead. The Go Lambda runtime library reports it through the normal channel. Also make sure the compiled bootstrap file sits at the zip root and was built with GOOS=linux and the function's GOARCH.
Can x86_64 native modules run in an arm64 Lambda?
No. Rebuild or install Linux aarch64 binaries (npm --arch=arm64, pip --platform manylinux2014_aarch64) and match the runtime version and Python ABI as well as the processor.
Why does a Lambda layer cause Runtime.ExitError?
A layer can carry native code built for the wrong architecture or runtime, or an extension that crashes. Check the exact layer version ARN, its folder layout, and whether the failure began when the layer was added.
Where do I find the REPORT line for Lambda Runtime.ExitError?
In the function's CloudWatch log stream, via Lambda Monitor, View CloudWatch logs, or aws logs tail /aws/lambda/function-name. Find the failing request ID; its REPORT line is the last line for that request.
Will increasing Lambda memory fix Runtime.ExitError?
Only when memory was the cause, which the REPORT line tells you: compare Max Memory Used with Memory Size. It does nothing for exit calls, wrong-architecture binaries, or a broken extension.
Why does my Lambda work locally but fail after deployment?
Your laptop's processor, operating system, dependency builds or runtime version differ from the deployed environment. Inspect the deployed artifact, confirm the function's runtime and architecture, and build native packages for that Linux target.
Why is Runtime.ExitError still happening after I removed process.exit?
Another exit path, a native dependency, an extension, or an initialization problem is still ending the process, or the alias still points at the old version. Publish and move the alias, then use a minimal diagnostic function and add components back one at a time.
Jake's crash was the process.exit(1) in the validation branch, reached because the booking form had stopped sending an order ID. Ethan replaced it with a thrown error, awaited the email, published a version, moved the alias, and then did the part Jake had not thought about: he checked which of Tuesday's failed bookings had already been written before replaying any of them. Two had. Those customers got one email each, not three. The shop's rule since then fits on the note by the till: a booking either goes through properly or fails where someone can see it.
📌 If you keep one line from this page
Runtime.ExitError says the process stopped; the line above the REPORT says why, and it is almost never memory.
Exit calls, wrong-chip binaries, crashed layers, then memory. Await what the customer needs, then publish, move the alias, test the alias.
Revision note. Written October 9, 2026, with the five-layer limit and the Managed Instances sizes in place. A stitch in time saves nine: the REPORT line you read now saves nine redeploys later.