Lambda "[Errno 13] Permission denied" and "EACCES: permission denied": Fix the Zip File Permissions (Python, Node.js, Bootstrap, Layers)

Logeshwaran
—

"[Errno 13] Permission denied: '/var/task/lambda_function.py'" on AWS Lambda means a file in your deployment package isn't readable by the user Lambda runs your code as. In Node.js the same problem reads EACCES: permission denied, open '/var/task/index.js'. Lambda runs your function as an ordinary, non-root user, so every file in your zip must be readable by everyone (permission 644), and every program or script you run must also be executable (755). A zip built on a machine with strict default permissions carries files marked 600, readable only by their owner, and Lambda can't open them. The fix is one command before zipping, chmod -R a+rX, or a short script that repairs the permissions inside an existing zip. Here's the surprise that sends people the wrong way: in Python this isn't an "Unable to import module" error at all. The file is in the right place with the right name, and Lambda reports a PermissionError. Your code is fine. The zip is carrying a lock you can't see.

Jake runs a phone repair shop. His friend Ethan, a developer, wrote him a Lambda function that resizes the photos customers upload, using a small helper script for the conversion. Ethan's new work laptop came locked down by his employer's IT team, and the first zip he built on it failed on the very first request with "[Errno 13] Permission denied." The same code, zipped on his old laptop, worked. Once he fixed that, the helper script failed with the same error number for a different reason. A week later a colleague's Go version of the function, packaged as a container image, failed to start at all. This page is what Ethan worked out: how Lambda's permissions work in plain English, every version of the error by runtime, how a file ends up unreadable, a two-minute check of any zip, the fixes for Linux, macOS and Windows, executables and helper scripts, custom runtimes, layers and container images, and the "permission denied" errors that are a different problem.

⚡ Quick Answer

• What it means → a file in your zip isn't readable by Lambda's non-root user. Plain English.

• The fix → chmod -R a+rX before zipping: files 644, folders and programs 755. Fix it.

• Check a zip without unzipping → zipinfo, or the checker script for any OS. Check.

• Helper programs, bootstrap, Go → they also need the execute bit. Executables.

• Errno 30 "Read-only file system" → different problem: write to /tmp. Look-alikes.

Not sure which message you have? Every version, by runtime.

New to Lambda? Our plain-English guide to AWS Lambda explains functions, packages and runtimes in about ten minutes. You can follow this page without it; every step starts from zero.

🧭 NEW HERE? READ THESE FIRST

New to Lambda packages? These five pages make the rest of this one easy:

📌 Bookmark this; the permissions table and the zip checker are the parts you will come back for.

What "Permission denied" means on Lambda, in plain English

Every file on a Linux system, which is what Lambda runs on, carries three sets of permissions: one for the file's owner, one for its group, and one for everyone else. Each set says whether that audience can read the file, write to it, and execute it as a program. The short form you'll see everywhere is a three-digit number, one digit per audience:

NumberShown asOwnerEveryone elseOn Lambda
644-rw-r--r--Read, writeReadRight for code and data files
755-rwxr-xr-xRead, write, runRead, runRight for folders, scripts, binaries, bootstrap
600-rw-------Read, writeNothingFails: Lambda can't read it
700-rwx------Read, write, runNothingFails for files
644 on a script-rw-r--r--Read, writeReadFails when you try to run it

When Lambda unpacks your zip into /var/task, it keeps each file's permissions as they were stored in the zip. Then it runs your function as an ordinary user, not as the owner of those files and not as an administrator. To that user, your files are "everyone else." So the last digit is the one that matters. A 600 file looks perfect on your laptop, where you're the owner, and is completely locked on Lambda.

Lambda's own rule is short: 644 for regular files, 755 for folders and anything executable. Folders get some help: if your package's folders aren't readable, Lambda sets them to 755 for you. Files don't get that help.

Jake: "So the file is in the box, but it's sealed with tape only Ethan's laptop can open."

Ethan: "Exactly like that. Lambda opens the shipping box fine. Then it finds a parcel inside addressed 'for the sender's eyes only,' and it's not allowed to open it."

Every version of the error, by runtime

The cause is always a missing permission bit, but each runtime reports it its own way. These are the messages from Lambda's Python 3.13, Node.js 22 and custom runtime images, running as a non-root user like Lambda does:

WhereWhat you seeMissing permission
Python handler or module"errorType": "PermissionError", [Errno 13] Permission denied: '/var/task/lambda_function.py'Read (file is 600 or 700)
Python, running a helper[Errno 13] Permission denied: '/var/task/bin/convert.sh'Execute (file is 644)
Node.js handler or module"errorType": "Error", EACCES: permission denied, open '/var/task/index.js'Read
Custom runtime or GoRuntime.InvalidEntrypoint, failed to exec /var/task/bootstrap, log says Permission deniedExecute on bootstrap
Container imageIMAGE Launch error: fork/exec ...: permission deniedExecute on the entrypoint
Layer[Errno 13] Permission denied: '/opt/python/shoplayer.py'Read, inside the layer

Notice the first row. Python's Lambda runtime turns import failures into Runtime.ImportModuleError, "Unable to import module," but a permission problem isn't an import failure; it's an operating-system error. So it arrives as a plain PermissionError with a Python traceback, and searching for "Unable to import module" fixes won't help. In Node.js the error object also carries "code": "EACCES" and "errno": -13, the same error number 13 that Python prints.

The path in the message is the key clue in every case. It names the exact file that's locked: your handler, a module it imports, a script it runs, or a file in a layer under /opt.

What it looks like in CloudWatch Logs

Because the failure happens while Lambda is loading your code, before your handler ever runs, it shows up in the function's logs during the startup phase. In Python, the runtime first prints a general warning and then the real error:

LAMBDA_WARNING: Unhandled exception. The most likely cause is an issue in the function code. However, in rare cases, a Lambda runtime update can cause unexpected function behavior. ...
[ERROR] PermissionError: [Errno 13] Permission denied: '/var/task/lambda_function.py'

Read past the warning. It's the runtime's general advice for any startup crash and says nothing about permissions; the [ERROR] line underneath is the one that names the file. In Node.js, the equivalent log line starts with ERROR Uncaught Exception and includes "code":"EACCES".

Because the code never finishes loading, every invocation fails, not just some. Each new execution environment tries to load the same package, hits the same locked file and gives up. Retrying, raising the timeout or adding memory changes nothing, and that's a quick way to tell this apart from errors that come and go under load: a permissions problem fails on the first request and on every request after it, until a fixed package is deployed.

Jake: "So it's not flaky. It's broken every single time."

Ethan: "Every single time, which is actually good news. Errors that always happen are the easy ones. Fix the zip once and it's fixed for good."

Find and fix it in five steps

Whatever the runtime, the same five steps get you from the error to a working deploy:

  1. Read the path in the message. It names the exact file Lambda couldn't open or run, under /var/task for your package or /opt for a layer.
  2. Decide which bit is missing. Code and data files need read for everyone; programs, scripts and bootstrap also need execute. Errno 30 instead of 13 means a different problem: see the look-alikes.
  3. Check the zip. Run zipinfo function.zip, or the checker script below, and look at the last three characters of each file's permissions.
  4. Fix the permissions where the zip is built. chmod -R a+rX on the package folder, chmod 755 on programs, then zip again; or rewrite an existing zip with the fixer script.
  5. Redeploy and invoke once. A test invocation right after update-function-code confirms the fix before real traffic arrives.

How a file ends up unreadable

Nobody sets 600 on purpose. It arrives from the environment where the package is built:

  • A strict umask. Every new file's permissions come from a setting called the umask. The common default gives new files 644. Security-hardened laptops, some company-managed machines and some build servers use 077 instead, which makes every new file 600 and every new folder 700. That includes files from git clone, pip install and npm install, because they all create files through the same setting.
  • Files copied from somewhere private, such as a key, certificate or config file from your home folder that was deliberately set to 600.
  • Archives that stored strict permissions, extracted and then zipped again.
  • Scripts and binaries without the execute bit, often because they were downloaded or copied rather than installed, or because Git only remembers the execute bit if it was set when the file was committed.

Here's a strict umask at work. These files were created with umask 077:

package:
-rw------- 1 root root    2 Oct 11 13:46 app.py
drwx------ 2 root root 4096 Oct 11 13:46 bin

package/bin:
-rwx------ 1 root root   18 Oct 11 13:46 run.sh

Every one of them works for its owner and fails on Lambda. That was Ethan's new laptop exactly: same code, stricter default.

Jake: "So the IT team's security setting broke my photos."

Ethan: "In a way, yes. Their setting is sensible for a laptop full of private files. It's just wrong for a package that a different user on another computer has to open."

Check any zip in two minutes

You can see the stored permissions without unzipping or deploying anything. On Linux and macOS, zipinfo lists them in the first column:

zipinfo function.zip
Archive:  function.zip
Zip file size: 534 bytes, number of entries: 3
-rw-------  2.0 unx      192 b- defN 80-Jan-01 00:00 lambda_function.py
-rw-------  2.0 unx       27 b- defN 80-Jan-01 00:00 shopcalc.py
-rw-r--r--  2.0 unx       25 b- defN 80-Jan-01 00:00 bin/convert.sh

The first two are 600 and will fail. The script is readable but not executable, so it fails as soon as the code tries to run it.

For a check that works on Windows too, and also knows which files should be executable, save this as zip_permissions.py. Given one zip, it reports; given a second file name, it writes a fixed copy:

"""Check, and optionally fix, the Unix permissions stored inside a Lambda .zip.

    python3 zip_permissions.py function.zip              # report
    python3 zip_permissions.py function.zip fixed.zip    # write a copy with 644 files and 755 executables
"""
import stat
import sys
import zipfile

EXECUTABLE_NAMES = ("bootstrap",)
EXECUTABLE_SUFFIXES = (".sh",)


def wants_exec(name, mode):
    base = name.rsplit("/", 1)[-1]
    return base in EXECUTABLE_NAMES or name.endswith(EXECUTABLE_SUFFIXES) or "/bin/" in "/" + name or bool(mode & 0o111)


src = zipfile.ZipFile(sys.argv[1])
problems = 0
for info in src.infolist():
    if info.is_dir():
        continue
    mode = (info.external_attr >> 16) & 0o777
    need = 0o755 if wants_exec(info.filename, mode) else 0o644
    ok = mode and (mode & need) == need
    if not ok:
        problems += 1
    print(f"{'ok ' if ok else 'BAD'}  {stat.filemode(stat.S_IFREG | mode) if mode else 'no-unix-mode'}  {info.filename}")
print(f"{problems} file(s) need fixing")

if len(sys.argv) > 2:
    with zipfile.ZipFile(sys.argv[2], "w", zipfile.ZIP_DEFLATED) as out:
        for info in src.infolist():
            data = src.read(info)
            if info.is_dir():
                info.external_attr = (stat.S_IFDIR | 0o755) << 16 | 0x10
            else:
                mode = (info.external_attr >> 16) & 0o777
                info.external_attr = (stat.S_IFREG | (0o755 if wants_exec(info.filename, mode) else 0o644)) << 16
            out.writestr(info, data)
    print(f"wrote {sys.argv[2]}")

On the zip above, it reports three problems:

BAD  -rw-------  lambda_function.py
BAD  -rw-------  shopcalc.py
BAD  -rw-r--r--  bin/convert.sh
3 file(s) need fixing

It treats a file as needing 755 if it's named bootstrap, ends in .sh, sits in a bin folder, or already has an execute bit for anyone. If your helper binaries live elsewhere or have other names, add them to EXECUTABLE_NAMES or EXECUTABLE_SUFFIXES. A file reported as no-unix-mode came from a tool that didn't record Unix permissions at all, which is common for zips made on Windows; the fixer gives those files explicit modes too.

The fix: set 644 and 755 before you zip

On Linux or macOS, one command gives every file read access for everyone and every folder, plus anything already executable, execute access too:

chmod -R a+rX package

The capital X matters. It adds execute only to folders and to files that are already executable for someone, so .py and .js files become 644 while scripts stay runnable. After running it on the folder above:

package:
-rw-r--r-- 1 root root    2 Oct 11 13:46 app.py
drwxr-xr-x 2 root root 4096 Oct 11 13:46 bin

package/bin:
-rwxr-xr-x 1 root root   18 Oct 11 13:46 run.sh

Exactly Lambda's rule: 644 for the code, 755 for the folder and the script. Then zip as usual from inside the folder, and the permissions travel with the files:

cd package
zip -r ../function.zip .

If a script or binary wasn't executable to begin with, give it the bit explicitly before zipping: chmod 755 package/bin/*.

Fix a zip you already have

If you only have the zip, from a build server, a colleague or a Windows tool, the script from the previous section writes a corrected copy:

python3 zip_permissions.py function.zip fixed.zip

Checking the new file shows every entry fixed:

ok   -rw-r--r--  lambda_function.py
ok   -rw-r--r--  shopcalc.py
ok   -rwxr-xr-x  bin/convert.sh
0 file(s) need fixing

The original zip, deployed, failed with [Errno 13] Permission denied: '/var/task/lambda_function.py'. The fixed copy ran and returned its result on the first try. Deploy it the usual way:

aws lambda update-function-code --function-name photo-resize --zip-file fileb://fixed.zip

Make it permanent in your build

Fixing one zip by hand fixes one deploy. To stop it coming back, put the chmod -R a+rX step into whatever builds your package: the build script, the CI job, or the Makefile, right before the zip command. If the build runs on a machine with a strict umask, you can also set a normal one for the build step only, with umask 022 at the top of the script, so every file the build creates starts out as 644.

Jake: "One command. That's it?"

Ethan: "One command, in the right place. Put it in the build script and nobody on any laptop can break it again."

Helper programs and scripts: the execute bit

Functions that call other programs, such as an image converter, a PDF tool or a shell script, need those programs to be executable, not just readable. Ethan's photo function ran a small script, and with the script stored as 644 it failed like this:

{"errorMessage": "[Errno 13] Permission denied: '/var/task/bin/convert.sh'", "errorType": "PermissionError"}

Same error number, different missing bit. With the script stored as 755, the same function returned {"out": "converted\n"}. The fix is chmod 755 on every program before zipping, or the zip fixer above, which treats anything in bin/ as executable.

If you can't rebuild the package right now, there's a runtime workaround. /var/task is read-only, so you can't change permissions there, but /tmp is writable. Copy the program there once and make it executable:

import os, shutil, subprocess

TOOL = "/tmp/convert.sh"


def handler(event, context):
    if not os.path.exists(TOOL):
        shutil.copy("/var/task/bin/convert.sh", TOOL)
        os.chmod(TOOL, 0o755)
    out = subprocess.run([TOOL], capture_output=True, text=True).stdout
    return {"out": out}

That ran with the script still stored as 644 in the zip. The copy happens once per execution environment, on the first request after a cold start, because /tmp survives between requests to the same environment. Treat it as a stopgap; fixing the package is cleaner.

Node.js: "spawn EACCES"

Node.js reports a non-executable helper differently from Python. Starting a 644 script with child_process.execFile gives:

EACCES | spawn /tmp/run.sh EACCES

The spawn in the message tells you Node was trying to run the file, not read it, so the missing bit is execute. The fixes are the same: chmod 755 before zipping, or copy the program to /tmp and fs.chmodSync(path, 0o755) it there before the first run.

Custom runtimes and Go: bootstrap must be executable

On the provided.al2023 runtime, which Go, Rust and custom runtimes use, Lambda starts your function by running a program called bootstrap. If that file isn't executable, nothing starts:

{"errorMessage": "failed to exec /var/task/bootstrap", "errorType": "Runtime.InvalidEntrypoint"}

The function's log shows the reason: /var/task/bootstrap: Permission denied. The same bootstrap stored as 755 started and answered normally.

On Linux and macOS, go build produces an executable file, so zipping it directly keeps the bit. Windows is the trap: Windows has no Unix execute bit, so a bootstrap zipped with Windows tools often arrives without one. The aws-lambda-go project includes a small tool for exactly this case, build-lambda-zip, which stores files in the zip with full read and execute permissions:

go install github.com/aws/aws-lambda-go/cmd/build-lambda-zip@latest
build-lambda-zip -o bootstrap.zip bootstrap

Run the second command from the folder that contains your compiled bootstrap, then deploy bootstrap.zip. The Python fixer above works as well; it marks any file named bootstrap as 755.

Container images: COPY the right permissions

Lambda also runs container images as a non-root user, so the same rule applies inside the image. A program copied in without the execute bit fails before your function starts. Developers who hit this report the error as:

IMAGE Launch error: fork/exec /usr/local/bin/docker-php-entrypoint: permission denied

Set the permissions in the Dockerfile itself, so it doesn't depend on the machine that builds the image. COPY --chmod does it in one line:

FROM public.ecr.aws/lambda/provided:al2023
COPY --chmod=755 bootstrap ${LAMBDA_TASK_ROOT}/bootstrap
CMD ["handler"]

Built with --chmod=644 instead, the same image failed with Runtime.InvalidEntrypoint when run as a non-root user; with --chmod=755 it answered normally. If your build tool doesn't support COPY --chmod, add RUN chmod 755 ${LAMBDA_TASK_ROOT}/bootstrap after the COPY. For code files copied into an image, RUN chmod -R a+rX ${LAMBDA_TASK_ROOT} covers everything at once.

Layers: the same rule under /opt

A layer is unpacked into /opt the same way your function is unpacked into /var/task, permissions included. A layer built on a strict-umask machine fails with the layer's path in the message:

[Errno 13] Permission denied: '/opt/python/shoplayer.py'

Run the same check and fix on the layer's zip before publishing it: python3 zip_permissions.py layer.zip fixed-layer.zip, then publish the fixed file as a new layer version. Functions keep using the layer version they were configured with, so after publishing, point each function at the new version. Our guide to "Unable to import module" shows the folder layout layers need, which is the other half of getting a layer right.

The fix for each build environment

The right fix depends on where the package is built. Here's the short version for each:

Where you buildFixNotes
Linux or macOSchmod -R a+rX package, then zipAdd umask 022 to build scripts on strict-umask machines
WindowsBuild and zip in WSL or a Linux container, or run the fixer on the finished zipWindows zip tools often store no Unix permissions at all
Go on Windowsbuild-lambda-zip -o bootstrap.zip bootstrapKeeps bootstrap executable
CI pipelineumask 022 at the start of the job, or a chmod step before zippingFresh runners can have a stricter default than your laptop
Container imageCOPY --chmod=755 for programs, RUN chmod -R a+rX for codeLambda runs images as a non-root user
LayerFix the layer zip, publish a new version, update functionsFunctions keep using the old version until you switch

Catch it at startup with a five-line self-check

If packages come from several people or tools, a small check at the top of your handler file can name every problem file in the function's logs the first time it starts, before any request fails in a confusing place:

import os


def unreadable_files(root="/var/task"):
    """List package files the function itself cannot read or run."""
    problems = []
    for folder, _, files in os.walk(root):
        for name in files:
            path = os.path.join(folder, name)
            if not os.access(path, os.R_OK):
                problems.append(path)
            elif "/bin/" in path and not os.access(path, os.X_OK):
                problems.append(path + " (not executable)")
    return problems


PROBLEMS = unreadable_files()
if PROBLEMS:
    print("Permission problems in the package:", PROBLEMS)

On a package with a 600 config file and a non-executable script, it reported both at once:

["/var/task/config.json", "/var/task/bin/convert.sh (not executable)"]

It runs once per cold start, because it sits outside the handler. The handler file itself has to be readable for the check to run at all, so this catches problems in everything else: config files, data files, modules imported later, and helper programs. Remove it once your build sets permissions reliably.

"But 600 is safer": secrets and permissions on Lambda

People often set a file to 600 on purpose, because it holds something sensitive: an API key, a service-account file, a certificate. On a shared laptop that's good practice. Inside Lambda it protects nothing and breaks the function, because the only user who could ever read the file is your function itself. Lambda execution environments are never shared across functions, and the microVMs they run in are never shared across AWS accounts. A 644 file in your package is readable by your function and nobody else on that machine.

The real risk with secrets in a zip is elsewhere. Anyone allowed to call GetFunction on your function receives a download link for the whole deployment package, and the zip itself tends to end up in build artifacts and repositories. Keep secrets out of the package entirely: store them in AWS Secrets Manager or Systems Manager Parameter Store and read them at startup. Our guide to AWS Secrets Manager explains how, and its companion on Secrets Manager versus Parameter Store covers which costs less.

Jake: "So locking the file was the right instinct in the wrong place."

Ethan: "Right instinct, wrong place. On Lambda the safe move is to not ship the secret at all. Fetch it when the function starts, and nobody who downloads the code ever sees it."

"Permission denied" errors that are a different problem

Several errors look like this one and need completely different fixes:

MessageWhat it really isFix
[Errno 30] Read-only file system: '/var/task/out.txt'Your code tried to write into the package folderWrite to /tmp instead
EACCES: permission denied, scandir '/mnt/...'An EFS file system's own file permissionsFix the EFS access point's user and folder permissions
AccessDenied or AccessDeniedExceptionAn AWS permission, from IAMFix the function's execution role policy
invalid ELF headerA binary built for another operating systemRebuild the dependency for Linux

The first one is the most common mix-up. Errno 13 means "you can't open this file"; Errno 30 means "nobody can write here." Lambda's package folder is read-only for everyone, by design, and only /tmp is writable. Our guide to Lambda's read-only file system covers that one, and our guide to "invalid ELF header" covers binaries built on the wrong platform. For EFS, the access point's user and folder settings decide what the function can open; our EFS mount guide covers the mount side.

Errno 13 in Python outside Lambda, on Linux and macOS

"[Errno 13] Permission denied" isn't special to Lambda. It's the operating system's standard answer whenever a program tries something its user isn't allowed to do, and Python reports it the same way everywhere. Three versions come up constantly on Linux and macOS machines and in containers.

Writing into a system folder. As an ordinary user, the system Python's own folders are read-only to you. A program that writes there, or pip install --target pointed at one, fails:

PermissionError: [Errno 13] Permission denied: '/var/lang/lib/python3.13/site-packages/x.txt'

Recent versions of pip notice this on a plain pip install and quietly install into your user folder instead, under ~/.local, so you often won't see the error from pip at all. The clean fix is a virtual environment, which belongs to you entirely:

python3 -m venv .venv
.venv/bin/pip install tabulate

Running sudo pip install makes the error go away too, but it mixes your packages into the system's, which is how system tools get broken. Use a virtual environment instead.

Running a script that isn't executable. The shell and Python say it differently:

sh: line 1: /tmp/run.sh: Permission denied
PermissionError: [Errno 13] Permission denied: '/tmp/run.sh'

The shell's version also exits with code 126, which in shell scripts always means "found it, but couldn't run it." chmod +x /tmp/run.sh fixes both.

Reading a file that belongs to another user, such as a key or log file set to 600 by someone else: you need that user's access, a group that can read it, or a copy you're allowed to read. That's the same situation Lambda is in with a 600 file in your zip.

When it happens on your own machine, these steps find it fast:

  1. Find the path in the message and run ls -l on it, and ls -ld on its folder.
  2. Check who you are with id, and compare with the owner and group in the listing.
  3. Pick the smallest fix: chmod +x for a script, a virtual environment for Python packages, a path in your home folder for output files.
  4. Reach for sudo last, and never for pip install into the system Python.

One look-alike: on Linux and macOS, trying to open() a folder as if it were a file gives a different error, IsADirectoryError: [Errno 21] Is a directory, not Errno 13. If you see 21, check that your path ends in a file name.

SAM, Serverless Framework, CDK and CI pipelines

Deployment tools build the zip for you, so they inherit the permissions of the files they find. They don't usually change permissions on their own, which means the fix still belongs before the tool runs:

  • In CI, add chmod -R a+rX on the source and dependency folders as a step before the package or deploy command, or set umask 022 at the start of the job.
  • Locally on a strict-umask laptop, check umask in your terminal. If it prints 0077, either run the build in a shell with umask 022 or keep the chmod step in your build script.
  • For any tool, check the zip it produced with zipinfo or the checker script before the first deploy. It takes seconds, and it tells you whether the tool kept your permissions.

Container-based builds, like sam build --use-container or a Docker build stage, sidestep the laptop's umask because the files are created inside the container. For images, the Dockerfile's COPY --chmod or RUN chmod line is the permanent fix.

Still failing? Troubleshooting by symptom

It works locally and fails on Lambda

Locally you're the file's owner; on Lambda you're "everyone else." Check the zip with zipinfo, and look at the last digit of each file's permissions.

The error names a file I didn't write

A dependency was installed with strict permissions. chmod -R a+rX on the whole package folder, including dependencies, fixes all of them at once.

The handler works but a script it runs fails

The script is readable but not executable. chmod 755 it before zipping, or copy it to /tmp and os.chmod it at runtime.

It started failing after I changed laptops or CI runners

The new machine has a stricter umask. Run umask there; 0077 explains it.

Only the container image version fails

Check the entrypoint and bootstrap permissions inside the image. Use COPY --chmod=755 or a RUN chmod line.

It's Errno 30, not 13

That's the read-only package folder. Write to /tmp instead.

Lambda Errno 13 permission denied: frequently asked questions

What does [Errno 13] Permission denied mean in AWS Lambda?

A file in your deployment package isn't readable, or a program isn't executable, by the non-root user Lambda runs your code as. The path in the message names the file. Set files to 644 and folders and executables to 755 before zipping.

How do I fix Errno 13 permission denied in Lambda?

Run chmod -R a+rX on the package folder before creating the zip, and chmod 755 any scripts or binaries. For an existing zip, rewrite it with corrected permissions using a small Python script, then deploy the fixed zip.

What permissions do Lambda deployment files need?

644 (rw-r--r--) for regular files and 755 (rwxr-xr-x) for folders and anything executable, such as scripts, binaries and bootstrap. Lambda fixes unreadable folders to 755 itself, but not files.

Why does Lambda say EACCES permission denied for index.js?

index.js is stored in the zip without read permission for other users, often 600 from a strict umask. Lambda's Node.js runtime can't open it. Run chmod -R a+rX before zipping.

Why does Lambda run my code as a non-root user?

Lambda runs functions as an ordinary user with limited rights, for safety. To that user, your files belong to someone else, so they need read permission for everyone.

Is Errno 13 the same as Unable to import module?

No. Unable to import module means Python couldn't find or load the module. Errno 13 means it found the file and wasn't allowed to read it; in Python it arrives as a PermissionError, not Runtime.ImportModuleError.

What is the difference between Errno 13 and Errno 30 on Lambda?

Errno 13 means a file can't be opened because of its permissions. Errno 30 means the code tried to write to a read-only location, usually /var/task. Fix 13 with chmod before zipping; fix 30 by writing to /tmp.

How do I check file permissions inside a zip?

On Linux and macOS, run zipinfo function.zip and read the first column. On any system, a short Python script that reads each entry's external_attr shows and can fix the stored permissions.

Why does my bootstrap fail with permission denied?

bootstrap must be executable (755). If it isn't, Lambda reports Runtime.InvalidEntrypoint, failed to exec /var/task/bootstrap. Zipping on Windows often loses the execute bit; build-lambda-zip from aws-lambda-go keeps it.

How do I zip a Go Lambda function on Windows?

Install build-lambda-zip with go install github.com/aws/aws-lambda-go/cmd/build-lambda-zip@latest, then run build-lambda-zip -o bootstrap.zip bootstrap. It stores the file with execute permission, which Windows zip tools usually don't.

Can I chmod files inside /var/task at runtime?

No. /var/task is read-only. Copy the file to /tmp, chmod it there, and run or read the copy. It's a workaround; fixing permissions before zipping is cleaner.

How do I fix permission denied in a Lambda container image?

Set permissions in the Dockerfile with COPY --chmod=755 for executables, or add RUN chmod -R a+rX on the code folder. Lambda runs images as a non-root user, so files must be readable and entrypoints executable by others.

Do Lambda layers need the same permissions?

Yes. Layers are unpacked into /opt with their stored permissions, and a 600 file fails with its /opt path in the message. Fix the layer zip, publish a new version, and update each function to use it.

What is umask and why does it matter for Lambda?

umask decides the permissions of new files. The usual 022 gives files 644; a strict 077 gives 600, which Lambda can't read. Check it with the umask command and use 022 for build steps.

Why did my Lambda deploy break after changing laptops?

The new machine probably has a stricter umask, common on company-managed laptops, so every file you create or install is 600. Add chmod -R a+rX to your build before zipping.

What does Errno 13 Permission denied mean in Python?

The operating system refused an action your user isn't allowed to do: reading a file without read permission, writing into a protected folder, or running a script without the execute bit. The path in the message is the file it refused.

How do I fix Errno 13 when installing Python packages?

Use a virtual environment: python3 -m venv .venv, then .venv/bin/pip install the package. Recent pip versions fall back to a user install automatically; the error usually means --target points at a protected folder.

What does spawn EACCES mean in Node.js?

Node tried to run a file that isn't executable. Set the execute bit with chmod 755 before packaging, or on Lambda copy the program to /tmp and chmod it there before running it.

Is 644 permission a security risk on Lambda?

No. Lambda execution environments are never shared across functions, so a 644 file is readable only by your own function there. Keep real secrets out of the zip anyway, in Secrets Manager or Parameter Store, because anyone who can download the package can read it.

How do I set exactly 644 on files and 755 on folders?

Run find package -type f -exec chmod 644 {} + for files and find package -type d -exec chmod 755 {} + for folders, then chmod 755 on your scripts and binaries. chmod -R a+rX does almost the same in one command and keeps executables runnable.

Can I fix the permissions in a zip without unzipping it?

Not with zip itself, but a short Python script can rewrite every entry's stored permissions into a new zip without extracting anything to disk: 644 for files, 755 for scripts, binaries and bootstrap. The zip_permissions.py script on this page does exactly that.

Does Lambda fix file permissions automatically?

Only for folders: Lambda sets folders it can't access to 755. Files keep the permissions stored in the zip, so unreadable files stay unreadable.

What does EACCES permission denied scandir /mnt mean in Lambda?

That's EFS, not your package. The EFS access point's user ID and folder permissions don't allow the function to read the folder. Fix the access point's POSIX user and root directory permissions.

For readers preparing for an AWS certification

We have been covering AWS certifications alongside our fixes, so here is a short extra for readers studying for one. If you only came for the fix, you already have it; feel free to skip this part.

Exams that test this topic: Developer – Associate (DVA-C03 from December 1, 2026).

Here are the exam-style questions this topic tends to raise, with the reasoning you need to answer them:

1. Why does a Lambda function get "Errno 13 Permission denied"?

It tries to write outside /tmp, or a bundled binary lacks execute permission.

2. How do you give a bundled binary execute permission?

Set the file mode to 755 before zipping; zips built on Windows often lose permissions.

3. Which user does Lambda code run as?

A restricted non-root user, so system paths are read-only.

4. How do you run a binary from a layer?

Place it under /opt/bin with execute permission; that path is on PATH.

5. Which deployment type preserves file permissions reliably?

Container images, built from a Dockerfile.

6. Can a function change file permissions at runtime?

Only for files it creates in /tmp.

Exam codes and dates change often. Our AWS certification changes page keeps track of what is current.

πŸ“š ALSO READ

Where to go next, from the read-only file system error to binaries built for the wrong platform:

📌 Bookmark this; the permissions table and the zip checker apply to every Lambda package you will ever build.

Ethan's build script now starts with umask 022 and runs chmod -R a+rX right before it zips, so the locked-down laptop builds the same package his old one did. The helper script lives in bin/ with its execute bit set, and his colleague's container image copies its bootstrap with --chmod=755. Jake's photos resize on the first try again. Ethan added a last line to the project's checklist, under "before you deploy," and read it out to Jake as a rule for anything that has to work on someone else's computer.

📌 If you keep one line from this page

Lambda opens your files as a stranger: before you zip, run chmod -R a+rX so every file is 644 and every folder and program is 755.

Errno 13 is a lock on the file; Errno 30 is a read-only folder. Check any zip with zipinfo before it ships.

Revision note. Written October 11, 2026, for every function that worked on the laptop and nowhere else. "A key that only fits your own pocket opens nothing for anyone else," as the old saying goes; give Lambda a key it can use.

Related