What to Do When Two Automations Trigger Each Other Into a Loop
Two automations that keep setting each other off create a runaway loop—rule A fires rule B, which fires rule A again, and around it goes. In smart-home reliability work, this is a positive-feedback race. It can flood a Zigbee or Z-Wave mesh, saturate an MQTT broker, or physically chew up a relay in minutes. For anyone running a home or small office on local control, a loop isn’t just irritating; it’s a failure mode that points straight at gaps in state management, trigger design, and how well you can see what your system is actually doing. This article walks through detection, root-cause analysis, and containment strategies you can apply without handing the keys to a cloud service.
How a Trigger Loop Forms in a Local System
A loop gets rolling when two or more automations share overlapping trigger conditions and overlapping actions, with nothing in place to break the chain. The classic example: a motion sensor turns on a light, and a lux sensor reading from that same light triggers a rule that dims or turns off the light. The light’s state change feeds right back into the motion rule, and the cycle repeats. On a local controller—Home Assistant, openHAB, Node-RED, or a custom Python daemon—the loop can run at CPU speed, far faster than any cloud-polling interval. That speed turns a logic mistake into a denial-of-service event on your own hardware.
Three ingredients make a loop possible:
- Shared state variable – a light’s on/off state, a temperature reading, a binary sensor.
- Bidirectional influence – automation A changes the state, automation B reads that state and changes it back.
- No hysteresis or deadband – the rules lack a threshold, delay, or condition that stops re-triggering within a short window.
In a local-control architecture, you own the entire event chain. That’s a strength because you can instrument it, but it’s also a risk because a loop can consume resources without any external rate limiter. Cloud-dependent hubs often mask the problem with server-side debouncing; you don’t get that safety net, so you have to build it yourself.
Detecting a Loop Before It Causes Damage
You’ll usually notice a loop by its symptoms: lights flickering rapidly, a relay clicking nonstop, or your controller’s CPU spiking. But small loops can be subtle—a temperature sensor reporting every second instead of every five minutes, or a log file growing 10× overnight. The first diagnostic step is to check the event stream. In Home Assistant, open Developer Tools → Events and subscribe to state_changed. In Node-RED, attach a debug node to the output of your function nodes. Look for repeated state transitions with identical old and new values, or a pattern that repeats with a fixed period.
If you run an MQTT broker like Mosquitto, use mosquitto_sub -v -t '#' to watch all topics. A loop often shows up as a topic being published dozens of times per second. For Zigbee2MQTT or Z-Wave JS, check the network map and device interview logs; a flooded network will show dropped messages and increased latency. The goal is to identify the exact pair (or chain) of automations that are feeding each other. Once you have the entities involved, you can trace the logic.
Root-Cause Analysis: State vs. Event Triggers
Most loops happen because an automation fires on every state change, even when the new state is the same as the old state, or when the change was caused by the automation itself. A motion sensor that reports “motion detected” every two seconds will re-trigger a rule that turns on a light—even if the light is already on. If that light’s state change triggers a second rule, you have the start of a loop.
Separate your triggers into two categories:
- Edge-triggered – fires only on a specific transition (e.g., from “off” to “on”).
- Level-triggered – fires whenever a condition is true, regardless of how it became true.
Loops thrive on level-triggered logic. A rule that says “if light is on, turn on fan” will fire every time the light’s state is reported as “on,” even if the fan is already on. If the fan’s state change then triggers a rule that affects the light, the cycle is self-sustaining. The fix is to use edge triggers where possible, or to add a guard condition that checks whether the action is actually needed.
Breaking the Cycle with Guard Conditions
A guard condition is a check that prevents an automation from running when its action would be redundant or counterproductive. The simplest guard is a state comparison: “Only turn on the light if it is currently off.” In YAML-based systems, this is a condition block. In Node-RED, it’s a switch node that routes messages only when the current state differs from the desired state.
For automations that must use level-triggered logic—such as a thermostat that adjusts based on a temperature sensor—add a deadband. A deadband is a range around the setpoint where no action is taken. For example, if the target is 21°C, you might heat only when the temperature drops below 20.5°C and cool only when it rises above 21.5°C. This prevents the system from oscillating between heating and cooling when the temperature hovers near the setpoint.
Another guard is a minimum cycle time. After an automation runs, block it from running again for a defined interval. In Home Assistant, you can use a delay action or a timer helper. In Node-RED, a delay node with “rate limit” mode works well. The interval should be long enough to let the physical system settle—for a light, 500 ms; for an HVAC damper, 30 seconds.
Using State Machines to Prevent Oscillation
A state machine defines a finite set of states and explicit rules for transitioning between them. Instead of writing two independent automations that can step on each other, you model the system as a single state machine. For example, a ventilation controller might have states: Idle, LowSpeed, HighSpeed. Transitions are triggered by CO₂ thresholds, but the machine only evaluates the next state from the current state. This eliminates the possibility of two rules fighting because there is only one active rule at a time.
In Home Assistant, you can implement a state machine with an input_select helper and automations that trigger on the helper’s state. In Node-RED, a function node can hold the current state in a context variable and only emit a command when a valid transition is requested. The key is that the state variable is the single source of truth; sensor readings are inputs, not direct triggers for actuators.
Practical Example: Motion-Controlled Lighting with Lux Feedback
Consider a common scenario: a motion sensor in a hallway turns on a light, and a lux sensor measures ambient light to decide whether the light should stay on. Without guards, the light turning on raises the lux reading, which tells the system the room is bright enough, so it turns off the light. The sudden drop in lux triggers the motion rule again, and the loop begins.
Here’s a reliable design:
- Motion sensor triggers only on a rising edge (no motion → motion).
- When motion is detected, check if the light is already on. If not, turn it on and start a 5-minute timer.
- The lux sensor is not used to turn off the light. Instead, it’s used to suppress the motion trigger: if ambient light is already above a threshold, the motion rule does nothing.
- When the timer expires, turn off the light unconditionally. The lux sensor is ignored during the off transition.
This design breaks the feedback path. The light’s own output never influences the decision to turn it off. The lux sensor only gates the initial trigger, not the ongoing state. If you need the light to stay on longer when it’s dark, adjust the timer based on the lux reading before the light turns on.

Tools for Loop Detection and Prevention
Several local-first tools include built-in features to catch loops. Home Assistant’s automation editor warns when an automation triggers itself, but it won’t catch cross-automation loops. For that, you need external analysis. The How to Audit the Small Systems That Quietly Run Your Week guide covers logging strategies that make loop patterns visible. Here are additional tool-specific techniques:
Home Assistant
Use the trace feature on an automation to see a timeline of trigger events, conditions, and actions. Run a trace during a suspected loop to see how many times the automation fires. Combine this with a counter helper that increments each time the automation runs; graph the counter in the history panel to spot runaway growth. For cross-automation loops, create a temporary input_boolean that each automation toggles; if the boolean’s state history shows rapid toggling, you’ve found the loop.
Node-RED
Attach a debug node to the output of each function or switch node and set it to log the full message object. Use a catch node to trap errors that may be side effects of a loop. The node-red-contrib-loop-detector package can monitor message rates and alert you when a flow exceeds a threshold. For manual analysis, inject a unique msg.id at the start of each flow and track it through the sequence; if the same ID appears in a loop, you’ve found the cycle.
MQTT-Based Systems
Use the retain flag carefully. A retained message can re-trigger a rule every time a subscriber reconnects, which can mimic a loop. Set retain: false on command topics unless you explicitly need the last-known-good value. Tools like MQTT Explorer let you visualize topic trees and spot rapid updates. If you see a topic updating faster than its sensor’s natural sample rate, a loop is likely the cause.
Designing Loop-Resistant Automations from the Start
The best loop fix is to never create one. Adopt these design habits for every automation you write:
- Single-responsibility principle – each automation should have one clear trigger and one clear outcome. Avoid chaining automations unless you explicitly model the chain as a sequence with timeouts.
- Idempotency – an automation should be safe to run multiple times with the same result. If turning on a light that’s already on causes no harm, a loop is less dangerous.
- Explicit state ownership – decide which automation “owns” each actuator. Only that automation may command the actuator. Other automations request changes via a scene controller or a virtual switch, which the owning automation reads.
- Rate limiting at the actuator – some devices (e.g., Shelly relays) support local rate limiting. Configure the device itself to ignore commands that arrive too quickly. This is a hardware-level safety net.

When a Loop Exposes a Deeper Architecture Problem
Sometimes a loop isn’t just a logic bug—it’s a symptom of a system that has grown without a clear control hierarchy. If you find yourself adding guard conditions to a dozen automations, step back and ask: who is in charge of each device? In a well-architected local system, each actuator has a single “driver” automation that owns its state. Other automations are “advisors” that suggest state changes but don’t command the actuator directly. This pattern, sometimes called a supervisory controller, prevents conflicting commands by design.
For example, instead of having three automations that can turn on a bathroom fan—one for humidity, one for motion, and one for a manual switch—create a single fan-controller automation. The humidity sensor, motion sensor, and switch all update a virtual “fan request” entity. The fan controller reads the request and decides whether to turn on the fan, applying its own minimum run time and off-delay. No loop can form because the fan’s state never feeds back into the request logic.
Testing Your Fixes Under Load
After you’ve added guards or restructured your automations, test them under conditions that would have triggered the loop. Don’t just test the happy path. Simulate rapid sensor changes: in Home Assistant, use the Developer Tools → States tab to manually set entity states in quick succession. In Node-RED, use an inject node to fire messages at high frequency. Watch the system’s response in real time. If you’ve added a rate limiter, verify that it actually caps the command rate at the actuator level, not just in the controller’s log.
For Z-Wave and Zigbee networks, a loop can cause congestion that persists even after the loop is broken. After fixing the logic, run a network heal (Z-Wave) or permit-join refresh (Zigbee) to rebuild routing tables. Check the controller’s event queue depth; if it’s backed up, a restart may be needed to clear stale messages.
Documenting Your Automations for Future You
A loop you fix today can reappear six months later when you add a new automation that interacts with the same entities. Document each automation’s intended behavior, its trigger conditions, and any guard conditions you’ve added. A simple text file in your configuration repository is enough. For each automation, note:
- Which entities it reads and which it controls.
- Whether it uses edge or level triggers.
- Any rate limits, deadbands, or timers.
- Which other automations it depends on or conflicts with.
This documentation doubles as a troubleshooting reference. When a new loop appears, you can quickly see which automations share state and where a feedback path might have been introduced.

FAQ
How can I tell if a loop is happening right now?
Check your controller’s CPU usage and event log. In Home Assistant, navigate to Developer Tools → Events and subscribe to state_changed. If you see the same entity changing state multiple times per second without a physical reason, a loop is likely. In Node-RED, look for debug messages repeating rapidly. For MQTT, use mosquitto_sub -v -t '#' and watch for a topic that updates far more often than expected.
Will adding a delay always fix a loop?
A delay can mask a loop but won’t fix the root cause. If two automations are still triggering each other, a delay just slows the cycle. The system may still oscillate, and the delay can introduce latency that makes the automation feel unresponsive. Use a delay only as a temporary measure while you redesign the logic to break the feedback path.
What’s the difference between a deadband and hysteresis?
In control systems, a deadband is a range around a setpoint where no action is taken. Hysteresis is a related concept where the threshold for turning on differs from the threshold for turning off. For example, a heater might turn on at 19°C and off at 21°C. The 2°C gap is hysteresis. Both techniques prevent rapid cycling, but hysteresis is specifically about asymmetric on/off points, while a deadband can be symmetric. For home automation, either term often describes the same practical fix: don’t act on small changes near the threshold.
Can a loop damage my hardware?
Yes. A relay that toggles hundreds of times per minute will exceed its rated mechanical life quickly. Solid-state relays and smart bulbs are more resilient, but the constant state changes can still cause premature failure. On the network side, a loop that floods a Z-Wave or Zigbee mesh can cause devices to drop offline, requiring a network repair. Treat a loop as a fault condition that needs immediate attention.