---
title: "High Availability (Active/Standby)"
description: "## The idea"
section: home-assistant
raw: "02-high-availability.md"
source: ai-generated
tags: home-assistant, home-automation
last-updated: 2026-09-15
---

# High Availability (Active/Standby)

> How two Home Assistant instances are run in an active/standby pair that fails over automatically — and the patterns that make two machines running identical config safe.

## The idea

Run two instances on the same LAN. The **active** instance serves the
dashboard, voice control, and automations; the **standby** waits, watching for
the active one to die. Failover happens automatically through two small
automations. When the active instance crashes (power loss, crash, reboot), the
standby notices within about a minute and promotes itself. When the old
instance recovers, it demotes itself — failback is automatic and symmetric,
with no manual "switch back" step and no split-brain.

```text
┌──────────────────────────┐   LAN, once per minute   ┌──────────────────────────┐
│Instance A (active)       │─────────────────────────►│Instance B (standby)      │
│                          │HTTP GET while A is active│                          │
│input_boolean.active=ON   │ (rest_command.heartbeat) │input_boolean.active=ON   │
│heartbeat timer           │                          │watchdog (restart mode)   │
└──────────────────────────┘                          └──────────────────────────┘
   A dies? → pats stop → B's watchdog fires → B becomes active
   B dies? → A's webhook calls fail harmlessly → nothing changes
```

Key state:

| Entity | Meaning |
|--------|---------|
| `input_boolean.active` | "I am the active instance". Each instance only ever sees *its own* boolean — it is not shared state. |
| `rest_command.heartbeat` | GET to the **partner's** webhook URL (kept in secrets). |
| Webhook | `local_only`, GET only — LAN traffic only. |

> The two instances do **not** share state: no database replication, no MQTT
> bus. Consistency comes entirely from the watchdog invariant — *only an
> instance that is not being patted may be active*.

## The two automations

### 1. Heartbeat timer — "pat the standby's watchdog"

While this instance is active (`input_boolean.active` is `on`), a
`time_pattern` trigger fires at the top of every minute and a state trigger on
`input_boolean.active` makes it fire **immediately** when the instance becomes
active after a restart. The action fires `rest_command.heartbeat` — a GET at
the partner's webhook.

### 2. Watchdog — the clever part

Mode is `restart`. Triggers are (a) a webhook hit from the partner, or (b)
`homeassistant.start`. The action is:

1. `input_boolean.active` → **off**
2. wait **65 s**
3. `input_boolean.active` → **on**

Because the mode is `restart`, every incoming heartbeat **re-arms the whole
sequence from the top**: the boolean goes off, the delay restarts, and the
boolean never gets to come back on. The 65 s delay is deliberately just longer
than the 60 s pat interval:

- **Partner alive** → a pat arrives every 60 s → the delay is re-armed before
  it can complete → the instance sits in standby. The
  `homeassistant.start` trigger arms the watchdog immediately at boot, so a
  freshly started instance *defaults to standby* and only promotes itself if
  the pats never arrive.
- **Partner dead** → the next pat never arrives → the last-armed 65 s delay
  runs to completion → `input_boolean.active` turns `on` → this instance is
  active. Its heartbeat timer starts patting the dead partner, which fails
  harmlessly.

**Failover timing:** because the delay is armed by the last successful pat,
failover completes at most about 65 s after the last pat — worst case roughly
a minute and five seconds after the failure, best case a few seconds.

## The critical pattern: gate automations on `input_boolean.active`

Because both instances run the *same* automation files, any automation that
must fire on **one machine only** carries a condition on
`input_boolean.active` being `on`. On the standby the boolean is `off` (the
watchdog keeps it that way), so the condition fails and the automation never
runs — no double-fired side effects, no split-brain.

```yaml
condition:
  - condition: state
    entity_id: input_boolean.active
    state: 'on'
```

Not every automation needs the gate. The rule of thumb:

- **Idempotent / hardware-tolerated actions** — skip the gate and let both
  instances run in parallel. Opening roller shutters at a set time sends the
  same command twice and the hardware ends up in the same place; turning a
  battery charger off at a set time is harmless to repeat.
- **Actions with global side effects** — SMS, TTS announcements,
  notifications, arming the alarm — *must* carry the gate, or every family
  member gets two texts.

## Shared config, per-instance secrets

Both instances pull from the **same git repository**. Anything that must differ
between the machines lives in each instance's local, git-ignored
`secrets.yaml` and is referenced with `!secret <name>` — the secret acts as a
per-instance variable inside the shared YAML:

| Secret | What varies per instance |
|--------|--------------------------|
| `name` | Instance name |
| `internal_url` / `external_url` | This instance's URLs |
| `heartbeat_url` | The **partner's** webhook URL |

Workflow: edit the shared YAML, commit, pull on both instances. Each instance
is also independently reachable — its own subdomain and its own nginx reverse
proxy on the same server (see [Deployment](/home-assistant/01-deployment/)).

## Gotchas worth knowing before you build this

- **Network reachability is a single point of failure.** A firewall or VLAN
  change between the two hosts looks identical to a dead partner — the standby
  will promote itself. Test after any router change.
- **The webhook must stay `local_only`-able.** Pats only work on the LAN; two
  instances on different subnets/VLANs need this revisited.
- **External assistants connect to one instance only.** A Google Assistant /
  HomeKit project can only meaningfully talk to *one* instance. With both
  instances exposing the same entities, the standby effectively has to be
  disconnected from the assistant (or you accept that requests may land on the
  wrong instance). Worth designing deliberately.
- **Single-connection hardware needs a device per instance.** Bluetooth
  bridges and similar can only be paired with one HA instance at a time — each
  instance needs its own dedicated bridge, and BLE devices only reach
  whichever instance owns the bridge.
- **Add failover alerting.** A useful extra automation: notify (SMS) when
  `input_boolean.active` flips to `on` — a promotion almost always means the
  other instance is down.

## Alternatives considered

- **MQTT heartbeat.** The original design published the local IP to an MQTT
  topic once per minute and had each instance watch the broker. It worked, but
  added a third moving part (the broker) that had to be up for the cluster to
  function. The webhook design removed the broker entirely: the "bus" is just
  the partner's own API, and the partner *is* the health check.
- **Keepalived / VIP failover.** Works, but moves the problem to the network
  layer and doesn't help with automations and integrations that are
  inherently per-instance (Google Home, BLE bridges, local webhooks). The
  in-HA watchdog keeps everything visible and debuggable inside the dashboard.

---

## Related

- [Deployment](/home-assistant/01-deployment/)
- [Automating with Google Home & HomeKit](/home-assistant/04-assistant-automations/)

---

## Source Disclaimer

- [x] AI Generated
- [ ] Human Generated
- [ ] AI Edited
- [ ] Human Edited
