OSError: [Errno 16] Device or resource busy in Python (tested)

OSError: [Errno 16] Device or resource busy (EBUSY) in Python usually means you tried to rename over, delete, or rmtree a mount point, most often a Docker bind-mounted file or directory. We reproduced each case and tested the fixes: write in place, mount the parent directory, or clear contents instead of the directory.

Setup: every block ran as docker run -v ./cfg.txt:/app/config.txt -v ./data:/app/data python:3-slim python bN.py, i.e. one bind-mounted file and one bind-mounted directory.

1. Atomic save onto a bind-mounted file

import os
# /app/config.txt is a single file bind-mounted from the host (-v ./cfg.txt:/app/config.txt)
with open("/app/config.txt.tmp", "w") as f:
    f.write("a=2\n")
os.replace("/app/config.txt.tmp", "/app/config.txt")    # atomic-save pattern

Output:

Traceback (most recent call last):
  File "/w/b1.py", line 5, in <module>
    os.replace("/app/config.txt.tmp", "/app/config.txt")    # atomic-save pattern
    ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OSError: [Errno 16] Device or resource busy: '/app/config.txt.tmp' -> '/app/config.txt'

Writing a temp file and then os.replace()/os.rename() onto the target is the usual safe-save pattern, and many libraries and editors do it for you. When the target is a mount point (a single file mounted with -v), the kernel refuses to replace it: EBUSY.

2. Deleting it fails too; writing in place works

import os
try:
    os.remove("/app/config.txt")
except OSError as e:
    print(f"remove: {type(e).__name__}: {e}")
with open("/app/config.txt", "w") as f:                   # writing in place works
    f.write("a=2\n")
print("in-place write ok:", open("/app/config.txt").read().strip())

Output:

remove: OSError: [Errno 16] Device or resource busy: '/app/config.txt'
in-place write ok: a=2

You cannot remove or replace a mount point, but you can open it and overwrite its contents. Either write in place (losing atomicity), or mount the directory that contains the file instead of the file itself, so the rename happens inside the mount.

3. shutil.rmtree on a mounted directory

import shutil
# /app/data is a bind-mounted directory (-v ./data:/app/data)
shutil.rmtree("/app/data")

Output:

Traceback (most recent call last):
  File "/w/b3.py", line 3, in <module>
    shutil.rmtree("/app/data")
    ~~~~~~~~~~~~~^^^^^^^^^^^^^
  File "/usr/local/lib/python3.15/shutil.py", line 859, in rmtree
    _rmtree_impl(path, dir_fd, onexc)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.15/shutil.py", line 721, in _rmtree_safe_fd
    _rmtree_safe_fd_step(stack, onexc)
    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.15/shutil.py", line 803, in _rmtree_safe_fd_step
    onexc(func, path, err, direntry=orig_entry, dir_fd=parent_fd)
    ~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.15/shutil.py", line 754, in _rmtree_safe_fd_step
    os.rmdir(name, dir_fd=dirfd)
    ~~~~~~~~^^^^^^^^^^^^^^^^^^^^
OSError: [Errno 16] Device or resource busy: '/app/data'

rmtree deletes the contents fine, then fails on the final rmdir of the mount point itself. Typical in Docker build/clean scripts that wipe a mounted /app/data or /output.

import os, shutil
# clear the mount point's contents but keep the directory itself
for name in os.listdir("/app/data"):
    p = os.path.join("/app/data", name)
    shutil.rmtree(p) if os.path.isdir(p) and not os.path.islink(p) else os.remove(p)
print("left:", os.listdir("/app/data"))

Output:

left: []

Clear the contents and keep the directory. (After the failed rmtree the files were already gone; it is only the mount point that cannot be removed.)

4. It is OSError, not PermissionError

import errno
try:
    import shutil; shutil.rmtree("/app/data")
except OSError as e:
    print(errno.errorcode[e.errno], e.errno, "| filename:", e.filename)
    print("PermissionError?", isinstance(e, PermissionError))

Output:

EBUSY 16 | filename: /app/data
PermissionError? False

Catch OSError and check e.errno == errno.EBUSY. Running as root does not help: the mount is the reason, not permissions. On Alpine (musl) the same errno reads differently:

OSError: [Errno 16] Resource busy: '/app/config.txt.tmp' -> '/app/config.txt'

Outside containers, Device or resource busy also appears when removing a directory that is a mount point on the host (an unmounted USB drive or NFS share that is still mounted). Run findmnt PATH or mount | grep PATH to confirm.

Related: shutil error handling ยท pathlib mkdir exceptions.

Frequently asked questions

Why does os.rename give Device or resource busy in Docker?

The target is a bind-mounted file (a mount point). It cannot be replaced; write to it in place or mount its parent directory instead.

How do I fix shutil.rmtree Device or resource busy?

The directory is a mount point. Delete its contents with a loop over os.listdir and keep the directory itself.

Is Errno 16 a permission problem?

No. It is OSError with errno EBUSY, not PermissionError, and it happens as root too.

Written with AI assistance. Measured on 2026-10-23 in Docker: python:3-slim (Python 3.15.1, glibc) and python:3-alpine (musl), with bind mounts from the host. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; Python is free.