Huny's Dev Blog
huny.dev

Atomic Config File Updates on Linux: Avoid Partial JSON with os.replace

Reproduce partial JSON during in-place rewrites. Use Python os.replace and fsync to publish complete files, and understand open readers, serialization failures, and durability limits.

Hun Jang
Hun Jang Nov 1, 2026

When updating a JSON configuration file read by a Linux service, build the new content in the same directory and replace it with os.replace rather than truncating and rewriting the destination. This avoids exposing an intermediate write to readers that open the file by name. Already-open files, competing writers, and persistence after power loss require separate handling. The Python example below demonstrates those boundaries.

Complete document cartridges and a swapping mechanism illustrate replacing a configuration snapshot. This is conceptual artwork, not an actual filesystem diagram.

Written and documentation checked: 2026-11-01. Execution environment: Python 3.12.14, Linux amd64, a local temporary directory. No user services or real configuration files were accessed.

Readers can open a file halfway through a rewrite

open(path, "w") truncates existing content before writing. If another piece of code opens the file after only the JSON prefix is written, it can see content that cannot be parsed. The reproduction below reads between two writes instead of relying on arbitrary delays or thread scheduling. It deterministically shows a state that a race could expose; it does not measure failure frequency.

The key is to keep incomplete content away from the name readers use. The Python os.replace documentation describes the atomicity of a successful rename. Creating the temporary file in the destination directory avoids moving across filesystem boundaries.

Replacement order and the purpose of each step

Step
Purpose
Serialize the document first
Leave the destination untouched if serialization fails
Write a temporary file in the same directory
Keep the destination name used by readers in place
Flush, then fsync the temporary file
Request Python-buffer flushing followed by file synchronization
Replace the name with os.replace
Publish the completed file under the destination name
Fsync the parent directory
Request synchronization of the changed directory entry

Atomicity describes the replacement readers observe. It differs from durability on storage. Linux fsync(2) explains that synchronizing a file alone does not ensure persistence of its parent directory entry; a separate directory call is needed. This example follows that order but has not undergone power-loss recovery testing.

A minimal runnable example

Save the following as atomic_config.py and run python atomic_config.py. No external packages are required. It assumes a trusted parent directory that stays stable during execution and one cooperating writer. It serializes the whole JSON document in memory, so it is not a streaming implementation for large files.

The example uses allow_nan=False to make NaN a serialization error. That does not validate required configuration fields or application-specific ranges. Perform the service's schema validation before publishing the temporary file.

Actual results and what readers observe

The five checks cover the in-place midpoint's parse failure, version 1 through an already-open file, version 2 through a new open, preservation of version 2 after serialization failure, and absence of leftover temporary files after normal execution. File and directory fsync calls also completed without errors in this run. I/O fault injection, forced termination, power loss, NFS, and Windows were not tested.

As explained in Linux rename(2), replacing a name does not redirect an existing open file descriptor to the new file. A reader should open once and read and parse through that handle. Reopening several times during one operation can mix versions. The existing handle reading version 1 in the example is not evidence of a failed replacement.

Conditions to address in production

  • An error after replacement does not mean rollback. If opening or synchronizing the directory fails after os.replace succeeds, the new content may already be visible even though the function raises. Callers must reread the version or content to establish the state. This error was not injected in the example.
  • Metadata needs its own policy. This code does not copy the existing file's permissions, owner, ACLs, or extended attributes. Check the temporary-file creation rules in the tempfile documentation, and set required metadata before publication if a service under another account must read it.
  • Atomic replacement is not an update lock. Multiple writers can overwrite each other's changes. Add locking or version checks, and do not treat several independent files as a single transaction.
  • Check paths and the deployment environment. This pattern does not defend against another user changing the parent directory or paths. Separately validate the target and behavior for symbolic links, hard links, mounted configuration files, and network filesystems.
  • Service reloads and temporary-file cleanup are separate. Replacing the file does not itself make a service reload its configuration. Forced termination can prevent finally from running, so clean up leftover temporary files according to an operational policy.

한국어 버전: Linux 설정 파일 갱신 중 JSON이 깨지는 이유: os.replace로 원자적 교체하기Linux 설정 파일 갱신 중 JSON이 깨지는 이유: os.replace로 원자적 교체하기

추천 글