What it means
A zip archive stores POSIX permission bits alongside each file, and Lambda preserves them when it extracts your package into /var/task. That is usually invisible and occasionally decisive: a file packaged without read permission for other users arrives unreadable, and the runtime cannot load it even though it is sitting exactly where the handler configuration says.
The reason this catches people is that everything looks correct. The file is in the archive. The handler string matches. Listing the package shows it there. Only the mode column distinguishes a working deployment from this error, and almost nobody looks at the mode column.
The runtime does not execute as the file's owner, so "readable by me" is not the property that matters — "readable by others" is. A file with mode 600 is entirely normal on a developer machine, where you own it, and unreadable in /var/task.
Two build environments produce this reliably. Containers: a build running as root inside Docker creates files owned by root, often with a restrictive umask, and those modes travel into the zip. Cross-platform archivers: some tools write no permission information at all, leaving the extractor to choose defaults that may not include read access. Both reproduce nowhere except in the pipeline that built them, which is why this tends to appear after a CI change that touched no code.
Directories are worth checking as well as files, because the failure they cause is misattributed. A directory without its execute bit cannot be traversed, so a perfectly readable file inside it is unreachable — and the error names the file, sending you to inspect permissions that are already correct. 644 for files, 755 for directories, applied in the packaging step, covers every version of this.
CloudWatch’s Errors metric: The error escaped your handler, so Lambda reports the invocation as failed and CloudWatch’s Errors metric counts it. LogStitch classified the invocation above as uncaught.
What it looks like in CloudWatch
This is the shape the failure arrives in: the lines of one invocation scattered among everything else the log group received at the same moment.
CloudWatch Logs Before
6 raw lines, in the order CloudWatch delivered them.
LogStitch After
The same lines, grouped into the invocation they belong to.
ErrorCold start · 184ms init84f2b7c1-0e39-4a62-b508-2d6c9f3a1e70
- 12:02:41.108PLAT
INIT_START Runtime Version: python:3.12.v41 Runtime Version ARN: arn:aws:lambda:us-east-1::runtime:9b1f5a2c
- 12:02:41.288ERROR
[ERROR] Runtime.ImportModuleError: Unable to import module 'lambda_function': [Errno 13] Permission denied: '/var/task/lambda_function.py'
- 12:02:41.288
Traceback (most recent call last):
- 12:02:41.402PLAT
START RequestId: 84f2b7c1-0e39-4a62-b508-2d6c9f3a1e70 Version: $LATEST
- 12:02:41.404PLAT
END RequestId: 84f2b7c1-0e39-4a62-b508-2d6c9f3a1e70
- 12:02:41.404PLAT
REPORT RequestId: 84f2b7c1-0e39-4a62-b508-2d6c9f3a1e70 Duration: 2.44 ms Billed Duration: 3 ms Memory Size: 256 MB Max Memory Used: 60 MB Init Duration: 184.09 ms Status: error Error Type: Runtime.ImportModuleError
The panel on the right is generated by running the excerpt on the left through the same parser that powers the free web stitcher — it is what the tool actually produces for this input, not an illustration of it.
How to confirm it from the logs
The path in the message is the giveaway: /var/task/ is where your deployment package is extracted, so this is your own code rather than anything of AWS's. It appears right after INIT_START, before any of your logging, because the file could not be read to be loaded.
Distinguish it from the file being absent. "Permission denied" means the runtime found it and was refused; "cannot find" or "no such file" means it was not there. They come from the same initialization step and have completely different fixes, and the archive listing tells you which you have in one command.
Causes, most likely first
The file was packaged without read permission for others
List the archive with its mode bits. The Lambda runtime does not execute as the file's owner, so a file with mode 600 is unreadable even though it looks fine locally where you own it. 644 for files and 755 for directories is what works.
The build ran as root with a restrictive umask
Check whether packaging happens inside a container. A Docker build with a tight umask, or files created by a root process, produces exactly this — and it reproduces nowhere else because the developer machine's umask is more permissive.
A directory in the path is not traversable
Check the mode on every directory in the archive, not just the file. A directory without the execute bit cannot be traversed, so a perfectly readable file inside it is unreachable and reports as a permission error on the file.
The archive was built by a tool that does not carry POSIX modes
Check what produced the zip. Some cross-platform archivers write no permission information at all, and the extracted result takes whatever default the extractor chooses — which may not include read access.
Fixes
Set modes before zipping, and verify them after
Normalising permissions in the packaging step is a two-line change that removes the whole class of problem, including the container and umask variants.
bashfind dist -type f -exec chmod 644 {} +
find dist -type d -exec chmod 755 {} +
cd dist && zip -qr ../function.zip . && cd ..
# The mode column should read -rw-r--r-- for files.
unzip -Z function.zip | head
Fix ownership when the build runs in a container
Files created by root inside a container commonly land with modes the runtime cannot use. Building as the invoking user avoids it entirely.
bashdocker run --rm \
--user "$(id -u):$(id -g)" \
-v "$PWD":/var/task \
public.ecr.aws/lambda/nodejs:22 \
npm ci --omit=dev
Check the whole path, not just the file
A readable file inside a non-traversable directory reports as a permission error on the file, which sends people to fix the wrong thing. Verify the directories too.
bashunzip -Z function.zip | awk '$1 ~ /^d/ { print }' | head
Also seen as
The same underlying failure, worded differently by a different runtime, SDK version, or logging layer. All of these land here — there is no separate page for each phrasing.
Related errors
Errors that show up alongside this one, or that people mistake for it.
References
- AWS Lambda Developer Guide — Troubleshoot deployment issues
- AWS Lambda Developer Guide — Deploy Lambda functions with .zip file archives
LogStitch finds this automatically, across every invocation in your account.
Paste a log excerpt into the free web stitcher and see it grouped, classified, and measured in your browser — nothing is uploaded. Or run the Mac app against your own AWS profiles and get the same view over every function you own.