How Home Assistant Ensures Single Instance Execution Per Config Directory

Home Assistant prevents multiple processes from using the same configuration directory by employing a POSIX file-lock around a hidden lock file (.ha_run.lock), implemented in homeassistant/runner.py and invoked during startup in homeassistant/__main__.py.

The home-assistant/core repository uses this mechanism to protect the SQLite database and other state files from concurrent writes. When you start Home Assistant, the system immediately attempts to acquire an exclusive lock on a file inside your configuration directory. If another process already holds this lock, the new instance aborts before initializing the runtime.

The Lock File Mechanism

Home Assistant stores its instance lock in a hidden file named .ha_run.lock, defined by the constant LOCK_FILE_NAME in homeassistant/runner.py at line 48. The ensure_single_execution() context manager coordinates all access to this file.

Creating the Lock File

When entering the ensure_single_execution() context, the system opens the lock file in append-plus mode ("a+"). This preserves any existing content while allowing the process to read and write file metadata. The implementation explicitly keeps the lock file on disk for the entire process lifetime and never unlinks it, preventing race conditions where two processes could create separate lock files.

Acquiring the Exclusive Lock

To claim ownership, Home Assistant calls fcntl.flock() with the flags LOCK_EX | LOCK_NB (exclusive, non-blocking). This requests an exclusive lock that fails immediately if another process holds it. According to the source code in homeassistant/runner.py (lines 36-40), a BlockingIOError is raised when contention occurs, triggering the failure path rather than blocking the process indefinitely.

Handling Lock Contention

When the lock cannot be obtained, the _report_existing_instance() function (lines 81-110) reads the lock file contents and prints a diagnostic error message. This message includes:

  • The PID of the running instance
  • The Home Assistant version currently executing
  • The Unix timestamp indicating when that instance started

The SingleExecutionLock context object sets exit_code = 1, allowing the caller to abort startup gracefully. In homeassistant/__main__.py (lines 90-96), the entrypoint checks this exit code immediately after the context manager and returns it to the shell, stopping the duplicate process before it initializes any components.

Successful Lock Acquisition

Upon successful acquisition, _write_lock_info() (lines 62-78) atomically stores four pieces of metadata in the lock file:

  1. Current process PID
  2. Lock-file format version
  3. Home Assistant version (from homeassistant/const.py)
  4. Unix timestamp of the start time

This metadata enables the _report_existing_instance() function to provide detailed conflict information if a second process attempts to start.

Integration with the Startup Flow

The lock manager integrates directly into the boot sequence. In homeassistant/__main__.py, immediately after validating the configuration path, the code enters the ensure_single_execution() context. Only after confirming exclusive access does Home Assistant proceed to construct the RuntimeConfig and invoke runner.run().

from homeassistant import runner

config_dir = "/home/user/.homeassistant"

with runner.ensure_single_execution(config_dir) as lock:
    if lock.exit_code is not None:
        # Another instance is already running – abort

        sys.exit(lock.exit_code)

    # Normal start-up continues here

    runtime = runner.RuntimeConfig(config_dir=config_dir, ...)
    runner.run(runtime)

Practical Implementation Examples

You can reuse this mechanism in standalone scripts that require exclusive access to a Home Assistant configuration directory:

#!/usr/bin/env python3
import sys
from homeassistant import runner

def main() -> int:
    config_dir = "/path/to/config"

    with runner.ensure_single_execution(config_dir) as lock:
        if lock.exit_code is not None:
            return lock.exit_code   # exit with 1 if another instance runs

        # …perform whatever work needs the config directory…

        print("Running with exclusive access")
        return 0

if __name__ == "__main__":
    sys.exit(main())

This pattern leverages the same fcntl.flock() implementation used by the core platform, ensuring your scripts respect the single-instance constraint.

Summary

  • Lock file location: A hidden file named .ha_run.lock inside the configuration directory, opened in "a+" mode to preserve metadata.
  • Locking primitive: fcntl.flock() with LOCK_EX | LOCK_NB flags provides exclusive, non-blocking POSIX locks.
  • Conflict resolution: On failure, _report_existing_instance() reads the existing lock metadata and returns exit code 1.
  • Metadata storage: Successful locks write the PID, format version, Home Assistant version, and timestamp via _write_lock_info().
  • Persistence: The lock file remains on disk for the process lifetime to prevent race conditions during file creation.

Frequently Asked Questions

What happens if I try to start two Home Assistant instances with the same config directory?

The second process detects the existing lock via fcntl.flock() in homeassistant/runner.py, invokes _report_existing_instance() to display the PID and version of the running instance, and exits with code 1 before initializing any components.

Why does Home Assistant use fcntl.flock() instead of checking for a PID file?

The fcntl.flock() mechanism is atomic and kernel-enforced, eliminating race conditions that can occur with simple PID file checks. Additionally, the lock automatically releases when the process terminates (even if it crashes), preventing stale lock scenarios without requiring complex signal handling.

Where is the single instance check performed in the codebase?

The check occurs in homeassistant/__main__.py immediately after configuration path validation, where the code enters the ensure_single_execution() context manager defined in homeassistant/runner.py. This ensures the lock is acquired before any database or state files are opened.

Can I disable the single instance execution check?

No, the check is mandatory and hardcoded into the startup sequence. The ensure_single_execution() function is called unconditionally in homeassistant/__main__.py to prevent data corruption from concurrent database access.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →