# How Home Assistant Ensures Single Instance Execution Per Config Directory

> Learn how Home Assistant ensures single instance execution per config directory using a POSIX file lock to prevent conflicts and protect your automation setup.

- Repository: [Home Assistant/core](https://github.com/home-assistant/core)
- Tags: internals
- Published: 2026-02-28

---

**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`](https://github.com/home-assistant/core/blob/main/homeassistant/runner.py) and invoked during startup in [`homeassistant/__main__.py`](https://github.com/home-assistant/core/blob/main/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`](https://github.com/home-assistant/core/blob/main/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`](https://github.com/home-assistant/core/blob/main/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`](https://github.com/home-assistant/core/blob/main/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`](https://github.com/home-assistant/core/blob/main/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`](https://github.com/home-assistant/core/blob/main/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()`.

```python
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:

```python
#!/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`](https://github.com/home-assistant/core/blob/main/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`](https://github.com/home-assistant/core/blob/main/homeassistant/__main__.py) immediately after configuration path validation, where the code enters the `ensure_single_execution()` context manager defined in [`homeassistant/runner.py`](https://github.com/home-assistant/core/blob/main/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`](https://github.com/home-assistant/core/blob/main/homeassistant/__main__.py) to prevent data corruption from concurrent database access.