Lambda "Invalid ELF Header" Fix: bcrypt, sharp and Python Packages Built on a Mac or Windows (Plus ELFCLASS32 and Exec Format Error)
"Invalid ELF header" on AWS Lambda means your deployment package contains a compiled file that was built for macOS or Windows instead of Linux. Lambda runs on Amazon Linux, opens the file expecting a Linux program, and finds a Mac or Windows one. In Node.js it reads /var/task/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: invalid ELF header; in Python it arrives as Runtime.ImportModuleError: Unable to import module 'lambda_function': ... invalid ELF header. The fix is to install your dependencies for Linux, either with pip or npm flags that ask for Linux files or by building inside Lambda's own container image, then zip and deploy again. Here is the surprise that sends people in circles: the same mistake produces at least five different error messages, and the strangest one says cannot open shared object file: No such file or directory about a file that is sitting right there in your package. That one means the file is Linux, but built for the wrong processor. And the pip command most guides hand you can quietly install a version of numpy that is three releases old.
Jake runs a phone repair shop. His friend Ethan, a developer, built him two small Lambda functions last year: a Node.js one that makes thumbnails of the photos customers upload of their cracked screens, and a Python one that encrypts the customer list every night. Both ran for months. Then Jake bought a new MacBook, updated one dependency, zipped the folder and uploaded it. The thumbnail function started failing with "invalid ELF header." He rebuilt it the way a forum answer suggested, and while he was at it, switched both functions to the cheaper arm64 processors. Now the Python function said a file was missing. He could see the file in the console. This page is what Ethan showed him that weekend: what an ELF header is, the five messages and what each one is really telling you, a two-minute way to find the bad file, the fixes for Python and Node.js (with the pip trap), arm64, Docker and container images, Go, the GLIBC cousin of this error, and layers.
New to Lambda? Our plain-English guide to AWS Lambda explains functions, runtimes and deployment packages in about ten minutes. You can follow this page without it; every step starts from zero.
What "invalid ELF header" means: Lambda opened a file that isn't a Linux program
Most of your Lambda code is plain text. A Python file or a JavaScript file runs anywhere, because the runtime reads it fresh every time. Some packages also carry a second kind of file: compiled machine code, ready for one specific operating system and one specific processor. These are the fast, heavy parts of popular libraries. bcrypt hashes passwords in compiled C++. sharp resizes images through a compiled image library. cryptography, numpy and pydantic all ship compiled pieces. In Python these files end in .so (or .pyd on Windows); in Node.js they end in .node.
Every operating system has its own format for compiled code. Linux uses ELF, short for Executable and Linkable Format. macOS uses Mach-O. Windows uses PE. The very first bytes of the file announce which one it is, like the label on a shipping box. That label is the ELF header: a short block at the start of every Linux binary that says "I am ELF, 64-bit, for this processor."
When your function starts, the Python or Node.js runtime asks Linux to load the compiled file. Linux reads the first few bytes, expects the ELF label, and finds a Mac or Windows label instead. It stops right there and reports invalid ELF header. Nothing in your code ran. The file was never even opened past its first line.
You can see the labels yourself. These are the first bytes of the same bcrypt 5.1.1 file, downloaded three ways:
| Built for | First bytes (hex) | What Linux sees |
|---|---|---|
| Linux (any processor) | 7F 45 4C 46 ("\x7fELF") | A valid ELF header; next it checks the processor |
| macOS (Apple Silicon or Intel) | CF FA ED FE | invalid ELF header |
| Windows | 4D 5A ("MZ") | invalid ELF header |
A short Python line prints those bytes for any file, which is handy when you have the file but no other tools:
python3 -c "import sys; print(open(sys.argv[1], 'rb').read(4))" node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node
A Linux build prints b'\x7fELF'. A Mac build prints b'\xcf\xfa\xed\xfe'. A Windows build prints b'MZ\x90\x00'.
Jake: "So my thumbnail code is fine. It's one file inside bcrypt that's in the wrong language."
Ethan: "Think of your repair bench. A customer brings in a phone and you grab a replacement screen from the drawer. If the part in the box is a laptop screen, you know before you even unwrap it. That's 'invalid ELF header.' The box said Mac, and Lambda only fits Linux parts."
Jake: "And the arm64 thing, where it said the file was missing?"
Ethan: "That's a phone screen for the right brand but the wrong model. The box says Linux, so it passes the first check. Then the connector doesn't match the processor, and Linux says it can't open it. We'll get to that one. It's the sneakier of the two."
One mistake, five error messages
Here is the part that makes this error so slow to fix. The cause is always the same, a compiled file built for a different machine, but the message you get depends on three things: which operating system the file was built for, which processor, and how the file is named. Each row below is the message Lambda's Python 3.13 and Node.js 22 runtimes print for that case.
| What you see | What it means | Usual cause |
|---|---|---|
.../bcrypt_lib.node: invalid ELF header.../_rust.abi3.so: invalid ELF header | The file is a Mac or Windows binary | Installed or zipped on a Mac or Windows PC |
/var/task/...so: cannot open shared object file: No such file or directory (a full path into your package) | The file is Linux, but for the other processor | arm64 files on an x86_64 function, or the reverse |
No module named '_cffi_backend'cannot import name '...' from '...' (unknown location) | Python skipped the file because its name says another platform | A Mac, Windows or arm64 wheel whose file name carries its platform |
wrong ELF class: ELFCLASS32 | The file is 32-bit Linux; Lambda is 64-bit | Built on a 32-bit ARM board such as an older Raspberry Pi setup |
version `GLIBC_2.38' not found | The file is right for Lambda's processor but needs a newer Linux | Compiled on a newer Linux desktop or CI machine |
Runtime.InvalidEntrypoint with exec format error | The program Lambda starts first is built for another platform | A container image or Go binary built on an Apple Silicon Mac |
libxcb.so.1: cannot open shared object file (a bare library name, no path) | Not this problem: a system library is missing from Lambda | A package that expects a desktop Linux, like opencv-python |
Two lines in that table deserve a second look, because they trip up careful people.
The "No such file or directory" line is not lying, exactly. Linux checks the processor type in the ELF header, and when it doesn't match, the loader treats the file as if it isn't there and reports the generic "can't open" message. The giveaway is the path: it points at a file inside /var/task, which is your own package. If you can find that exact file in your zip, you have the wrong-processor case.
The last line looks almost identical but means something else. When the message names a bare library like libxcb.so.1 or libGL.so.1 with no folder in front, your package is fine and is asking for a system library that Lambda doesn't include. opencv-python 5.0.0 does exactly this; its sibling opencv-python-headless imports cleanly on the same runtime. That case has its own fixes, which we cover in the troubleshooting section.
How the error is labeled also differs by language. Python wraps it: the runtime catches the import failure and reports "errorType": "Runtime.ImportModuleError" with "errorMessage": "Unable to import module 'lambda_function': /var/task/cryptography/hazmat/bindings/_rust.abi3.so: invalid ELF header". Node.js 22 passes it through raw, with "errorType": "Error" and the file path as the whole message, followed by a stack trace that starts at Node's module loader. Either way, the path in the message is the clue that matters.
Why it happens: pip and npm install for the machine they run on
Package managers are polite. When you run pip install cryptography or npm install bcrypt, they look at the machine you are sitting at and fetch the compiled files that fit it. On a MacBook with an M-series chip, that means macOS files for arm64. On a Windows laptop, Windows files for x86_64. That is exactly right for running the code on that machine, and exactly wrong for zipping it and sending it to Lambda.
Python packages arrive as wheels, zip files whose names spell out what they are built for. The same cryptography release comes in many flavors:
cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl, for Macs with Apple Siliconcryptography-50.0.2-cp311-abi3-win_amd64.whl, for 64-bit Windowscryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl, for Linux on x86_64cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl, for Linux on arm64
The manylinux part is a promise: "this runs on any Linux whose C library (glibc) is at least version 2.17." Lambda's Amazon Linux 2023 runtimes (Python 3.12 and later, Node.js 20 and later) have glibc 2.34, so a manylinux wheel for the right processor is what you want.
Node.js packages use three different approaches, and which one a package uses decides how it breaks:
- Download at install time. bcrypt 5.1.1 runs a small installer that downloads one binary for your current machine into
lib/binding/napi-v3/bcrypt_lib.node. Install on a Mac, get a Mac file. This is the classic "bcrypt invalid ELF header" case. - Ship every platform, pick at runtime. bcrypt 6.0.0 includes ready-made files for seven platforms in a
prebuildsfolder (macOS arm64 and x64, Linux arm, arm64 and x64, Windows arm64 and x64) and chooses the right one each time it loads. A bcrypt 6 package built on a Mac runs on Lambda unchanged. - One small package per platform. sharp keeps its compiled code in separate packages such as
@img/sharp-darwin-arm64and@img/sharp-linux-x64, and npm installs only the ones that match your machine. Install on a Mac and the Linux one is missing.
So the error shows up in a handful of very ordinary situations:
- You installed dependencies on a Mac or Windows PC and zipped the folder.
- You built on an Apple Silicon Mac (arm64) for a function that runs on x86_64, or switched a working function to arm64 without rebuilding.
- Your Dockerfile installs dependencies correctly, then a later
COPY . .pastes your Mac'snode_modulesover them. - Your CI runner uses a different processor than your function, for example an arm64 runner building for an x86_64 function.
- Someone added a dependency on their laptop and committed
node_modulesor a vendoredpackagefolder to the repository.
Jake: "I didn't change anything except buying a new laptop."
Ethan: "That's the whole story for most people. Your old laptop was probably Intel, so the files it fetched were wrong for Lambda too, but your old zip still had the Linux files I built for you last year. The first time you reinstalled on the Mac, npm swapped them for Mac ones."
Find the bad file in two minutes
Before you fix anything, find out exactly which file is wrong and what it was built for. It saves you from rebuilding the whole project for the wrong target. Here is the order that works:
- Copy the path from the error. Everything after
/var/task/is the file's location inside your zip. For layers it starts with/opt/instead. - Check which processor your function uses. From the command line:
It printsaws lambda get-function-configuration --function-name photo-thumbs --query Architectures --output textx86_64orarm64. If you never chose one, it's x86_64, the default. - List the compiled files in your zip.
unzip -l function.zip | grep -E '\.(so|node|pyd)$' - Check what each file was built for. On a Mac or Linux machine, the
filecommand reads the header for you. Unzip into a scratch folder and point it at the file from step 1. - Compare. The file must be Linux, 64-bit, for the same processor as the function.
Here is what file prints for the bcrypt binary in each of the four builds:
bcrypt_lib.node: Mach-O 64-bit arm64 bundle
bcrypt_lib.node: PE32+ executable for MS Windows 6.00 (DLL), x86-64, 7 sections
bcrypt_lib.node: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked
bcrypt_lib.node: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked
Only the last line fits an x86_64 function; only the third fits an arm64 one.
If you have many dependencies, checking files one by one gets old fast. This small script, which uses nothing but Python's standard library and runs on Windows, macOS and Linux alike, walks a folder and labels every compiled file. Save it as check_binaries.py:
import pathlib
import sys
MACHINES = {62: "x86_64", 183: "arm64", 40: "arm32", 3: "x86-32"}
def describe(path):
head = path.read_bytes()[:20]
if head[:4] == b"\x7fELF":
return MACHINES.get(int.from_bytes(head[18:20], "little"), "other-cpu")
if head[:4] in (b"\xcf\xfa\xed\xfe", b"\xce\xfa\xed\xfe", b"\xca\xfe\xba\xbe"):
return "macOS"
if head[:2] == b"MZ":
return "Windows"
return "unknown"
folder = pathlib.Path(sys.argv[1])
want = sys.argv[2] if len(sys.argv) > 2 else "x86_64"
for path in sorted(folder.rglob("*")):
if path.is_file() and (path.suffix in (".so", ".node", ".pyd", ".dylib") or ".so." in path.name):
found = describe(path)
if found == want:
mark = "ok "
elif "prebuilds" in path.parts:
mark = "spare"
else:
mark = "BAD "
print(f"{mark} {found:<9} {path.relative_to(folder)}")
Run it on the folder you zip, with your function's architecture as the second argument:
python3 check_binaries.py package x86_64
For a Python package installed on an Apple Silicon Mac, the output looks like this:
BAD macOS _cffi_backend.cpython-313-darwin.so
BAD macOS cryptography/hazmat/bindings/_rust.abi3.so
BAD macOS pydantic_core/_pydantic_core.cpython-313-darwin.so
And the same package built correctly for Linux:
ok x86_64 _cffi_backend.cpython-313-x86_64-linux-gnu.so
ok x86_64 cryptography/hazmat/bindings/_rust.abi3.so
ok x86_64 pydantic_core/_pydantic_core.cpython-313-x86_64-linux-gnu.so
The script reads two things from each file: the first four bytes, which say Linux, Mac or Windows, and for Linux files the two bytes at position 18, the "machine" field, where 62 means x86_64 and 183 means arm64. Files inside a prebuilds folder are marked "spare" rather than "BAD," because packages like bcrypt 6 carry files for every platform on purpose and load only the one that fits. As long as one file in each prebuilds folder says "ok," that package is fine.
Jake: "Mine says BAD next to every single file."
Ethan: "Good. That means one fix covers all of them. If it had said BAD next to one, I'd go looking for the one package someone installed by hand."
Fix it in Python: three ways to get Linux wheels
The goal is a folder of dependencies built for Lambda's Linux and your function's processor, which you then zip with your code. There are three good ways to get it, and you only need one.
| Method | What you need | Best when | Watch out for |
|---|---|---|---|
| pip with --platform | pip only | All your packages publish Linux wheels | Fails on packages that only ship source; the manylinux2014 trap below |
| uv with --python-platform | uv (a single download) | You want the newest versions with the least typing | Same rule: wheels only for other platforms |
| pip inside the Lambda image | Docker | Some packages need compiling, or you want zero guesswork | arm64 builds on an x86 machine need emulation |
Option 1: pip with --platform
pip can fetch wheels for a machine other than the one it runs on. You tell it the platform, the Python version and the folder, and you tell it to accept only ready-made wheels, because it can't compile source code for another machine:
pip install \
--platform manylinux_2_28_x86_64 \
--platform manylinux2014_x86_64 \
--implementation cp \
--python-version 3.13 \
--only-binary=:all: \
--target package \
-r requirements.txt
Change 3.13 to your function's Python version. For an arm64 function, use manylinux_2_28_aarch64 and manylinux2014_aarch64. The two --platform lines are not a typo, and the next section explains why leaving out the first one is a trap.
Then add your own code and zip it, with the dependencies at the top level of the zip:
cd package
zip -r ../function.zip .
cd ..
zip function.zip lambda_function.py
And deploy:
aws lambda update-function-code --function-name nightly-encrypt --zip-file fileb://function.zip
If pip stops with No matching distribution found, one of your packages doesn't publish a Linux wheel for that Python version. Use option 3 for that project.
Option 2: uv with --python-platform
uv is a fast Python package manager that understands Lambda-style targets directly. You name the processor and the glibc version, and it accepts every wheel that fits, old tags and new:
uv pip install \
--python-platform x86_64-manylinux_2_34 \
--python-version 3.13 \
--target package \
-r requirements.txt
For arm64, use aarch64-manylinux_2_34. The 2_34 matches the glibc version in Lambda's Amazon Linux 2023 runtimes. With uv 0.13 and a requirements file of numpy and cryptography, this pulled numpy 2.5.4, cryptography 50.0.2 and their dependencies (cffi and pycparser), all as Linux x86_64 wheels, and the result imported cleanly on Lambda's Python 3.13 runtime. Zip and deploy exactly as in option 1.
Option 3: pip inside the Lambda image
The most reliable method skips the guessing entirely: run pip inside the same Linux that Lambda uses. AWS publishes its runtime images publicly, so Docker can borrow them for one command:
docker run --rm -v "$PWD":/var/task -w /var/task --entrypoint pip \
public.ecr.aws/lambda/python:3.13 install -r requirements.txt -t package
This mounts your project folder into the container, runs the image's own pip (26.2 in the October 2026 image), and writes Linux wheels into package on your machine. pip prints a warning about running as root; inside a throwaway container that's harmless. Because it really is Linux, packages that need compiling get compiled correctly too. Zip and deploy as before.
On an Apple Silicon Mac, Docker runs the arm64 version of the image by default. Add --platform linux/amd64 after docker run when your function is x86_64, so the wheels come out for the right processor.
Jake: "Which one would you use?"
Ethan: "On your Mac, uv. It's one line and it picks the newest versions. If a package ever refuses, the Docker line, because it can't be wrong about Linux. The plain pip line is fine too, as long as you keep both platform flags."
The pip --platform trap: manylinux2014 can quietly give you an old version
Almost every guide to this error gives you the same pip command with a single --platform manylinux2014_x86_64. It works. That's the problem: it works so quietly that you don't notice what it did.
Here is the same request, numpy for Python 3.13 on Linux x86_64, asked three ways with pip 26.1 in October 2026:
| --platform value | What pip installed |
|---|---|
manylinux2014_x86_64 | numpy 2.2.6 (three releases behind) |
manylinux_2_28_x86_64 | numpy 2.5.4 (the newest) |
manylinux_2_34_x86_64 | Nothing: "No matching distribution found for numpy" |
Newer numpy releases publish Linux wheels only for glibc 2.27 and later, tagged manylinux_2_28. pip doesn't treat manylinux2014 as "glibc 2.17 or newer, anything up to my target." It treats it as a fixed name and widens it only to the two older names, manylinux2010 and manylinux1. So it walks back through numpy's history until it finds a release that still had a manylinux2014 wheel, and installs that. No warning, no error. Your function runs, just on an older numpy than your laptop has, which surfaces weeks later as a missing function or a behavior difference.
The third row is the mirror image. You might think the cleanest choice is the exact glibc Lambda has, 2.34. But pip takes manylinux_2_34_x86_64 literally too, and since numpy doesn't publish a wheel with that exact tag, nothing matches at all.
The fix is to list more than one platform. pip accepts --platform several times and treats the list as "any of these":
pip install --platform manylinux_2_28_x86_64 --platform manylinux2014_x86_64 \
--implementation cp --python-version 3.13 --only-binary=:all: --target package numpy
That pair covers the two tags nearly every Linux wheel uses today. If a package ever names another glibc level, add it as a third flag. uv avoids the whole question, because it reads x86_64-manylinux_2_34 as "anything that runs on glibc 2.34," which is why option 2 picked up numpy 2.5.4 without any help.
Jake: "So the forum answer I copied wasn't wrong. It was just old."
Ethan: "Right. When it was written, every package still published the 2014 tag. Packages moved on; the answer didn't. If you want to see what you actually got, look at the folder names in package: each one ends in the version, like numpy-2.5.4.dist-info."
Fix it in Node.js: bcrypt, sharp and other native modules
Node.js gives you more ways to get this wrong and, happily, more ways to get it right. Start with the package that causes most of these errors.
bcrypt: upgrade to version 6, or build inside the Lambda image
bcrypt 5.x downloads one binary for the machine you install on. That's why a bcrypt 5.1.1 package from a Mac or Windows PC fails on Lambda with:
{"errorType":"Error","errorMessage":"/var/task/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: invalid ELF header","trace":["Error: /var/task/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: invalid ELF header"," at Object..node (node:internal/modules/cjs/loader:1939:18)", ...]}
bcrypt 6.0.0 changed how it ships. It carries ready-made binaries for seven platforms, including Linux x86_64 and arm64 in both glibc and musl flavors, and picks the right one when it loads. Its install step only builds from source when no ready-made file fits your machine, so an install on a Mac still carries the Linux files, and a bcrypt 6 package hashed a password on Lambda's Node.js 22 runtime without complaint. If your code works with bcrypt 6, this is the smallest fix there is:
npm install bcrypt@6
Zip and deploy as usual. The prebuilds folder adds about 1 MB for the platforms you don't use, a small price for never thinking about this again.
If you need to stay on bcrypt 5, or you have other native modules, install inside Lambda's Node.js image so every package sees Linux:
docker run --rm -v "$PWD":/var/task -w /var/task --entrypoint npm \
public.ecr.aws/lambda/nodejs:22 ci --omit=dev
This uses your package-lock.json, installs production dependencies only, and leaves a Linux node_modules in your project folder. npm ci removes any existing node_modules before it starts, so nothing from your laptop survives. As with Python, add --platform linux/amd64 on an Apple Silicon Mac when the function is x86_64. A project with bcrypt 5.1.1 and sharp 0.35.5 installed this way ran on Lambda with both modules loading their native Linux code.
Then zip from inside the project folder, so index.js and node_modules sit at the top of the zip:
zip -r function.zip index.js package.json node_modules
sharp: install the Linux package explicitly
sharp keeps its compiled code in per-platform packages, and npm can be told which platform to install for, even when you are on a Mac:
npm install --os=linux --cpu=x64 sharp
Use --cpu=arm64 for an arm64 function; with that flag, npm installed @img/sharp-linux-arm64 and @img/sharp-libvips-linux-arm64. When the Linux package is missing entirely, sharp's error is unusually helpful. This is the full message from Lambda's Node.js 22 runtime:
Could not load the "sharp" module using the linux-x64 runtime
Possible solutions:
- Ensure optional dependencies can be installed:
npm install --include=optional sharp
- Ensure your package manager supports multi-platform installation:
See https://sharp.pixelplumbing.com/install#cross-platform
- Add platform-specific dependencies:
npm install --os=linux --cpu=x64 sharp
- Consult the installation documentation:
See https://sharp.pixelplumbing.com/install
One quirk can hide the problem instead. With sharp 0.35.5 and npm 10.9, an install for macOS also pulled in @img/sharp-wasm32, sharp's WebAssembly build, as a side effect of other optional packages. On Lambda, sharp found no Linux binary, fell back to WebAssembly, and the function worked. It worked more slowly, though: resizing a 3000 by 2000 pixel JPEG to 400 pixels wide took about 35 milliseconds with the WebAssembly build and about 24 milliseconds with the native Linux one on the same processor. That's roughly 1.5 times slower, on every image, billed by the millisecond. You can check which one you got with Object.keys(sharp.versions).includes("emscripten"), which is true only for the WebAssembly build. Install the Linux package and the fallback never kicks in.
Other native modules
Any npm package with compiled code behaves like one of the three types above: download at install time like bcrypt 5, ship every platform like bcrypt 6, or split into per-platform packages like sharp. The checker script tells you which situation you're in, and the Docker line fixes all three at once. If a package compiles itself during install (you'll see node-gyp in the install output), it needs a Linux machine to build on, such as the Docker method, because your laptop's compiler makes files for your laptop.
Jake: "My thumbnails are sharp. My login check is bcrypt 5."
Ethan: "Then the Docker line, once, fixes both. And next time you touch the login code, move to bcrypt 6 so the Mac stops mattering."
arm64 (Graviton) functions: "cannot open shared object file" for a file that is right there
Lambda offers two processors: x86_64, the default, and arm64, AWS's Graviton chips. arm64 costs less for the same run time: when this was written in October 2026, AWS's price list for US East (N. Virginia) showed $0.0000166667 per GB-second on x86_64 and $0.0000133334 on arm64, 20% less at the first pricing tier. That's why so many people switch, and why this section exists. Moving a function from one to the other changes what its compiled files must be built for. Your Python and JavaScript don't care; every .so and .node file does.
When an x86_64 function loads an arm64 file, or the other way around, the message is the confusing one:
{"errorMessage": "Unable to import module 'lambda_function': /var/task/cryptography/hazmat/bindings/_rust.abi3.so: cannot open shared object file: No such file or directory", "errorType": "Runtime.ImportModuleError"}
Node.js reports the same thing for bcrypt 5's arm64 binary on an x86_64 function:
{"errorType":"Error","errorMessage":"/var/task/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: cannot open shared object file: No such file or directory"}
The file exists. Linux read its ELF header, saw a valid Linux binary, then compared the processor field with its own and found a mismatch. At that point the loader gives up on the file as if it had never found it, and the "No such file or directory" text is the generic message for that outcome. A binary built for any other processor gets the same treatment; it isn't special to arm64. That's why people spend an hour checking zip paths and folder names for a file that was never missing.
There is one more variant on the Python side. Many Python binaries carry their platform in the file name, like _cffi_backend.cpython-313-aarch64-linux-gnu.so. Python on an x86_64 function only looks for names ending in .cpython-313-x86_64-linux-gnu.so, .abi3.so or plain .so, so it never even tries the arm64 file and reports No module named '_cffi_backend' instead. Same cause, third message.
To build for arm64, swap the target in whichever method you use:
- pip:
--platform manylinux_2_28_aarch64 --platform manylinux2014_aarch64 - uv:
--python-platform aarch64-manylinux_2_34 - npm for sharp:
npm install --os=linux --cpu=arm64 sharp - Docker: add
--platform linux/arm64todocker run. On an Apple Silicon Mac this runs natively; on an Intel or AMD machine Docker has to emulate arm64, which is slower.
You can change the architecture and the code in one step, which avoids a window where the new architecture runs the old package:
aws lambda update-function-code --function-name nightly-encrypt --zip-file fileb://function.zip --architectures arm64
To confirm from inside a running function which processor you're on, Python's platform.machine() returns x86_64 or aarch64, and Node's process.arch returns x64 or arm64. Logging it once at startup makes this whole class of error obvious in CloudWatch.
Jake: "So when I switched to arm64, my Mac files weren't even the problem anymore. My Linux files were, because they were x86."
Ethan: "Exactly. You fixed the box label and then changed the phone model. Rebuild for arm64 and both functions are happy."
Invalid ELF header in Docker and container images
Containers are supposed to end this problem, since the build happens inside Linux. Two mistakes bring it right back.
COPY . . pastes your laptop's node_modules over the Linux one
This Dockerfile looks right. It copies the package files, installs dependencies inside the Lambda image, then copies the code:
FROM public.ecr.aws/lambda/nodejs:22
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["index.handler"]
If your project folder on the Mac has its own node_modules, the last COPY . . copies it too, right over the Linux one that npm ci just built. The image builds without a warning and fails on the first request with /var/task/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: invalid ELF header. This is the usual story behind "invalid ELF header docker" searches, and it isn't specific to Lambda; any Node.js image built this way breaks the same way.
The fix is one line in a file called .dockerignore, next to your Dockerfile:
node_modules
Docker then leaves your local node_modules out of the build, and the image keeps the Linux one. The same project, rebuilt with that file in place, ran cleanly. For Python projects, add your local virtual environment and any package folder you built on the host to .dockerignore for the same reason.
An image built for arm64, deployed to an x86_64 function
On an Apple Silicon Mac, docker build makes an arm64 image unless you say otherwise. Push it and create an x86_64 function from it, and Lambda can't even start the container. Developers who hit this report the error as:
"errorType": "Runtime.InvalidEntrypoint",
"errorMessage": "RequestId: ... Error: fork/exec /lambda-entrypoint.sh: exec format error"
/lambda-entrypoint.sh is the first program in every AWS base image, and "exec format error" is Linux refusing to run a program built for another processor. Build for the function's architecture explicitly:
docker buildx build --platform linux/amd64 --provenance=false -t photo-thumbs:latest .
Use linux/arm64 for an arm64 function. Keep --provenance=false too; Lambda requires it for images built with docker buildx. Then tag, push to Amazon ECR and update the function as you normally would. If containers are new to you, our guide to what Amazon ECR is covers the push step from zero.
Go and custom runtimes: exec format error
Go compiles your whole function into one program, so there is no library to mismatch, only the program itself. On Lambda's current custom runtime, provided.al2023, that program must be named bootstrap and built for Linux and the function's processor. Go makes this easy, because it cross-compiles with two environment variables:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -tags lambda.norpc -o bootstrap main.go
zip function.zip bootstrap
Use GOARCH=arm64 for an arm64 function. Leave out GOOS=linux on a Mac and you get a Mac program; leave out GOARCH on an Apple Silicon Mac and you get an arm64 one. Either way, the provided.al2023 runtime image reports Runtime.InvalidEntrypoint and its log shows /var/task/bootstrap: cannot execute binary file: Exec format error. With Go 1.27 and aws-lambda-go 1.55, the amd64 build above answered on the provided.al2023 runtime image, while the arm64 and Mac builds of the same code both failed to start on that x86_64 runtime with exactly that error.
CGO_ENABLED=0 is worth the extra words. When you build for the same platform you're on, for example on a Linux desktop, Go turns on its C bridge by default and links the program against your machine's C library. That build needed glibc 2.34 symbols, which happened to match Lambda's Amazon Linux 2023 runtime, and would have failed on an older Linux. With CGO_ENABLED=0 the program is statically linked and doesn't care which glibc Lambda has. The -tags lambda.norpc flag leaves out an older RPC mode that provided.al2023 doesn't use; for this small function it cut the program from 13.1 MB to 10.1 MB.
The handler setting doesn't matter on provided.al2023; Lambda runs bootstrap. If you see a handler error instead, our guide to Runtime.HandlerNotFound covers the other runtimes.
When the file is Linux but too new: "GLIBC_2.38 not found"
Sometimes the binary passes every check on this page: Linux, 64-bit, right processor. And it still fails:
/lib64/libc.so.6: version `GLIBC_2.38' not found (required by /var/task/newglibc.so)
That file was compiled on a Linux machine with a newer C library than Lambda's. Linux binaries record the newest glibc feature they use, and the loader refuses a binary that asks for more than the system has. A small library compiled on a Linux desktop with glibc 2.38 or later, calling a string function added in that version, failed exactly like this on both the Python 3.13 and Node.js 22 runtimes. Lambda's runtimes split into two families:
| Runtime | Operating system | glibc |
|---|---|---|
| Python 3.12, 3.13, 3.14 | Amazon Linux 2023 | 2.34 |
| Node.js 20, 22 | Amazon Linux 2023 | 2.34 |
| provided.al2023 (Go, Rust, custom) | Amazon Linux 2023 | 2.34 |
| Python 3.10, 3.11 | Amazon Linux 2 | 2.26 |
This is exactly what the manylinux tag protects you from. A manylinux_2_28 wheel promises to need nothing newer than glibc 2.28, which fits Amazon Linux 2023's 2.34 and does not fit Amazon Linux 2's 2.26. That's one more reason to stop at uv's x86_64-manylinux_2_34, or at the two pip flags above, rather than compiling on whatever Linux you happen to have. If you're still on Python 3.11 or older, moving to 3.13 raises your glibc ceiling and opens up the newest wheels. Our guide to GLIBC_2.28 not found on Amazon Linux 2 walks through the same error on EC2 and older runtimes in depth.
Lambda layers get the same error, at a different path
A layer is a zip of shared dependencies that Lambda unpacks into /opt. Everything on this page applies to layers too: build them for Linux and the right processor, or they fail the same way, with a path that starts with /opt/python/ or /opt/nodejs/node_modules/ instead of /var/task/.
Two layer habits prevent most surprises:
- Label the architecture when you publish.
aws lambda publish-layer-versionaccepts--compatible-architectures x86_64orarm64, so the layer's details say which processor it was built for, months later, when nobody remembers. - Build layers the same way you build functions. The pip, uv and Docker commands above work unchanged; only the target folder changes, to
pythonfor Python layers andnodejs/node_modulesfor Node.js ones.
If the layer's files load but Python still can't find the module, the folder layout inside the layer is the usual suspect. Our guide to "Unable to import module" on Lambda shows the layer layouts that work, with the exact paths for each runtime.
"wrong ELF class: ELFCLASS32" and ELFCLASS64, outside Lambda too
The ELF header also records whether a binary is 32-bit or 64-bit. When the two sides disagree, Linux says so plainly:
/var/task/node_modules/bcrypt/lib/binding/napi-v3/bcrypt_lib.node: wrong ELF class: ELFCLASS32
ELFCLASS32 means a 64-bit program tried to load a 32-bit file. On Lambda, which is 64-bit on both processors, that file almost always came from a 32-bit ARM machine, such as a Raspberry Pi running a 32-bit operating system, or a package's 32-bit linux-arm build copied by hand. bcrypt's 32-bit ARM build, dropped into a bcrypt 5 package, produced exactly this message on Lambda's Node.js 22 runtime. Rebuild on a 64-bit machine or with any of the methods above.
ELFCLASS64 is the mirror image: a 32-bit program loading a 64-bit file. It can't happen on Lambda, but it's a common message elsewhere, for example a 32-bit Java or Python on a 64-bit server loading a 64-bit native library, or a 32-bit build of a program picking up the system's 64-bit libraries. The cure is the same idea in reverse: make the program and its libraries the same width, usually by installing the 64-bit version of the program.
The plain "invalid ELF header" message turns up outside Lambda for the same reason it turns up inside: a Mac or Windows binary on a Linux machine. Copying node_modules from a Windows folder into WSL, or mounting a Mac project folder with its node_modules into a Linux container, both produce it. In every case the fix is to reinstall the packages on the Linux side rather than copy them across.
Still failing? Troubleshooting by symptom
It works locally and fails only on Lambda
That's the signature of this whole error family: your laptop runs its own build perfectly. Run check_binaries.py on the exact folder you zip, not on your project folder, since they can differ. If everything says "ok," the problem isn't architecture; read the full error message again and check the handler and module path.
It failed right after I switched the function to arm64
Your package is still x86_64. Rebuild with an arm64 target and deploy it with update-function-code --architectures arm64, which switches the processor and the code together. Remember any layers too; each one needs an arm64 build.
It failed after I upgraded from Python 3.11 to 3.13
Compiled Python files are often tied to one Python version by name, like .cpython-311-x86_64-linux-gnu.so. Python 3.13 doesn't look for those names, so you get "No module named" for a module that is plainly in the zip. Rebuild with --python-version 3.13.
"No module named 'pydantic_core._pydantic_core'" or "cannot import name 'ArgsKwargs'"
Both point at the same thing: pydantic's compiled core was built for another platform. With pydantic-core 2.50.0, Mac, Windows and arm64 builds all produced cannot import name 'ArgsKwargs' from 'pydantic_core._pydantic_core' (unknown location) on an x86_64 Python 3.13 function. Rebuild with any of the three Python methods.
"cannot import name 'exceptions' from 'cryptography.hazmat.bindings._rust'"
That's cryptography 50 installed on Windows. Windows builds end in .pyd, which Linux Python ignores, so Python finds only a folder of type hints with the same name and reports the import as coming from an "unknown location." Rebuild for Linux.
"libxcb.so.1" or "libGL.so.1": cannot open shared object file
A bare library name with no path means a system library is missing, not that your file is wrong. With OpenCV, switch from opencv-python to opencv-python-headless, which skips the desktop display libraries; version 5.0.0 headless imported cleanly on Lambda's Python 3.13 runtime where the regular package failed on libxcb.so.1. For other libraries, use a container image and install the library in the Dockerfile.
The zip is now too big
bcrypt 6 carries spare binaries, and big libraries like OpenCV or numpy add up quickly. If you hit Lambda's package size limits, move the heavy dependencies into a layer or switch to a container image. Our guide to Lambda's size limits and workarounds covers the options.
Lambda invalid ELF header: frequently asked questions
What does invalid ELF header mean?
It means Linux tried to load a compiled file and its first bytes weren't the ELF label every Linux binary starts with. The file is usually a macOS (Mach-O) or Windows (PE) binary. On AWS Lambda, it means your package contains a dependency installed on a Mac or Windows PC.
What is an ELF header?
It's the short block at the start of every Linux executable and shared library. It begins with the bytes 7F 45 4C 46 ("\x7fELF") and records whether the file is 32-bit or 64-bit and which processor it's for, such as x86_64 or arm64. Linux reads it before loading anything else.
How do I fix invalid ELF header in AWS Lambda?
Reinstall your dependencies for Linux and your function's processor, then zip and deploy again. Use pip with --platform manylinux_2_28_x86_64 and --platform manylinux2014_x86_64, uv with --python-platform x86_64-manylinux_2_34, or run pip or npm inside the public.ecr.aws/lambda image with Docker.
Why does bcrypt give invalid ELF header on Lambda?
bcrypt 5.x downloads a binary for the machine you install on, so an install on a Mac or Windows PC puts a non-Linux bcrypt_lib.node in your package. Upgrade to bcrypt 6, which ships Linux binaries and picks the right one at runtime, or run npm ci inside the Lambda Node.js image.
How do I fix invalid ELF header in Docker?
Add node_modules to a .dockerignore file next to your Dockerfile. Without it, COPY . . copies your laptop's node_modules over the Linux one that npm ci built inside the image. For Python, ignore your local virtual environment and any package folder built on the host.
Why does Lambda say cannot open shared object file when the file exists?
The file is a Linux binary for the other processor, usually arm64 on an x86_64 function or the reverse. Linux rejects a processor mismatch with its generic "No such file or directory" message. Check the function's architecture and rebuild your dependencies for it.
What does wrong ELF class: ELFCLASS32 mean?
A 64-bit program tried to load a 32-bit library. On Lambda, which is always 64-bit, the 32-bit file usually came from a 32-bit ARM machine such as a Raspberry Pi with a 32-bit system. Rebuild the dependency on a 64-bit machine or with pip, uv or Docker targeting Lambda.
What does wrong ELF class: ELFCLASS64 mean?
It's the reverse: a 32-bit program tried to load a 64-bit library. It doesn't happen on Lambda, but it's common with 32-bit Java or Python installs on 64-bit servers. Install the 64-bit version of the program so it matches its libraries.
How do I install Python packages for Lambda on a Mac?
Use uv pip install --python-platform x86_64-manylinux_2_34 --python-version 3.13 --target package -r requirements.txt, or pip with two --platform flags and --only-binary=:all:, or run pip inside public.ecr.aws/lambda/python with Docker. Use the aarch64 versions for arm64 functions.
What is manylinux?
It's a tag on Python wheels meaning "runs on any Linux with at least this glibc version." manylinux2014 and manylinux_2_17 mean glibc 2.17 or newer; manylinux_2_28 means 2.28 or newer. Lambda's Amazon Linux 2023 runtimes have glibc 2.34, so both fit.
Why does pip --platform manylinux2014_x86_64 install an old version?
pip treats manylinux2014 as a fixed name and only widens it to manylinux2010 and manylinux1. Newer releases of packages like numpy publish only manylinux_2_28 wheels, so pip falls back to an older release. Add --platform manylinux_2_28_x86_64 alongside it.
How do I fix exec format error on Lambda?
The program Lambda starts is built for another platform. For container images, build with docker buildx build --platform linux/amd64 --provenance=false (or linux/arm64). For Go, build with GOOS=linux and GOARCH matching the function, into a file named bootstrap.
How do I build a Go Lambda function on a Mac?
Run CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -tags lambda.norpc -o bootstrap main.go, zip the bootstrap file, and deploy it to a provided.al2023 function. Use GOARCH=arm64 for an arm64 function. Without GOOS and GOARCH you get a Mac program.
How do I check my Lambda function's architecture?
Run aws lambda get-function-configuration --function-name your-function --query Architectures --output text. It prints x86_64 or arm64. Functions created without choosing one use x86_64, the default.
Do arm64 Lambda functions need different packages?
Only the compiled parts. Pure Python and JavaScript run on both, but every .so and .node file must be built for arm64. Rebuild with --platform manylinux2014_aarch64, uv's aarch64-manylinux_2_34, npm --cpu=arm64 or Docker --platform linux/arm64.
Is invalid ELF header the same as Unable to import module?
In Python they arrive together: the runtime reports Runtime.ImportModuleError, "Unable to import module," and the ELF message after the colon. "Unable to import module" alone has other causes too, such as a wrong handler path or a missing package, which our separate guide covers.
Why does sharp fail on Lambda?
sharp installs only the binary package for your current machine, so a Mac install has no Linux build. Run npm install --os=linux --cpu=x64 sharp (or --cpu=arm64), or install inside the Lambda Node.js image. A Mac install may fall back to sharp's slower WebAssembly build instead of failing.
How do I fix GLIBC not found on Lambda?
The binary was compiled on a Linux with a newer glibc than Lambda's 2.34 (or 2.26 on Python 3.11 and older). Install manylinux wheels instead of compiling, build inside the Lambda image, or move older Python functions to 3.12 or later.
Jake rebuilt both functions that Saturday. The thumbnail function got a fresh node_modules from inside the Lambda image; the encryption function got its wheels from uv, for arm64 this time. The checker said "ok" on every line, both functions ran on the first try, and the arm64 switch knocked a little off the monthly bill. Ethan added two lines to Jake's deploy notes: rebuild for Linux every time, and log the processor at startup. Then he wrote the line below on the whiteboard behind the repair bench, where Jake would see it the next time he bought a laptop.
📌 If you keep one line from this page
Lambda runs Linux on one processor; build every compiled dependency for that exact pair, never for the laptop you happen to be typing on.
"Invalid ELF header" means the wrong operating system; "No such file or directory" for a file you can see means the wrong processor.
Revision note. Written October 11, 2026, for everyone whose function broke the day they bought a new laptop. "A tool is only as good as the hand that fits it," the old saying goes; a binary is only as good as the machine it was made for.