Home Assistant Event Bus Architecture: How Events Are Dispatched
Home Assistant uses a single, thread-safe EventBus attached to the HomeAssistant core object to dispatch events via asynchronous listener jobs, enabling non-blocking pub/sub communication across all integrations.
The event bus architecture in Home Assistant serves as the central messaging backbone of the platform, allowing integrations, services, and core components to communicate without direct dependencies. Implemented in homeassistant/core.py within the home-assistant/core repository, this architecture guarantees thread-safe event registration and dispatch through a combination of listener registries, job wrappers, and context-aware Event objects.
Core Components of the Event Bus
The EventBus Class and Listener Registry
The EventBus class definition resides at [line 1422 of homeassistant/core.py](https://github.com/home-assistant/core/blob/dev/homeassistant/core.py#L1422). It maintains a _listeners registry implemented as a defaultdict that maps specific event types to lists of listener jobs (lines 1427–1435). This registry stores tuples containing a HassJob wrapper and an optional filter, enabling the bus to execute callbacks in the correct execution context.
A special MATCH_ALL list within the registry captures listeners that subscribe to every event type, allowing system-wide event monitoring.
Event Objects and Context Metadata
Every dispatched message travels as an Event instance, defined at line 1283. These objects carry:
- event_type: The identifier string (e.g.,
"state_changed") - data: The payload dictionary
- origin: An
EventOriginenum indicating local or remote source - context: Execution context for tracking event chains
- time_fired: UTC timestamp
Thread Safety and HassJob Wrappers
To prevent blocking the event loop, the bus wraps all callbacks in HassJob objects. When an event fires, the bus calls self.hass.async_run_hass_job, ensuring the listener executes in Home Assistant’s executor thread pool. This design isolates slow user code from the core async loop.
How Events Are Dispatched in Home Assistant
Listener Registration via async_listen
Registering a listener stores a job reference in the registry without blocking:
listener = hass.bus.async_listen("my_event", my_callback)
The async_listen method creates a HassJob from the provided callback or coroutine and appends it to the _listeners dictionary under the specified event type key.
Firing Events from Any Thread
The EventBus provides two entry points for thread safety:
From outside the event loop, use EventBus.fire (lines 1556–1568). This method marshals the call into the event loop using call_soon_threadsafe, ensuring thread-safe access from background threads.
From inside the event loop, use await hass.bus.async_fire (lines 1570–1588). This performs validation via _verify_event_type_length_or_raise before calling the internal dispatcher.
Internal Dispatch Pipeline
The async_fire_internal method (lines 1590–1600) executes the actual dispatch:
- Validates the event type string length.
- Instantiates an
Eventobject with the current timestamp and context. - Iterates over listeners registered for that specific type plus the
MATCH_ALLsubscribers. - Schedules each
HassJobviaasync_run_hass_jobfor execution.
Because dispatching schedules jobs rather than awaiting them directly, a slow listener cannot block subsequent event processing.
Special Listener Patterns
One-Time Listeners with async_listen_once
For single-use callbacks, the _OneTimeListener wrapper (lines 9395–9402) automatically removes itself from the registry before executing the user callback. This prevents memory leaks for one-off event handling:
hass.bus.async_listen_once("my_custom_event", handle_once)
Match-All Event Subscriptions
To receive every event on the bus, subscribe using the wildcard "*" (internally MATCH_ALL):
hass.bus.async_listen("*", all_events_handler)
This registers the listener in the special match-all list that async_fire_internal checks on every dispatch.
Practical Implementation Examples
Listening to Custom Events
def my_handler(event):
_LOGGER.info("Received %s with data %s", event.event_type, event.data)
# Register persistent listener
hass.bus.async_listen("my_custom_event", my_handler)
Firing Events from Integrations
From async context:
await hass.bus.async_fire(
"my_custom_event",
{"value": 42, "source": "sensor"},
origin=EventOrigin.local,
)
From a background thread:
hass.bus.fire(
"my_custom_event",
{"value": 42, "source": "sensor"},
)
One-Time Event Handling
async def handle_once(event):
_LOGGER.debug("One-time handler received: %s", event.data)
# Auto-removes after first execution
hass.bus.async_listen_once("startup_complete", handle_once)
Summary
- Single EventBus instance: Lives on
hass.businhomeassistant/core.pyand manages all system communication through a centralized registry. - Thread-safe dispatch:
fire()marshals calls to the event loop, whileasync_fire()validates and dispatches directly, both utilizingasync_fire_internalfor execution. - Job isolation: Listeners execute as
HassJobinstances in the executor thread pool, preventing slow callbacks from blocking the event bus. - Flexible subscription: Supports persistent listeners, one-time auto-removing listeners via
_OneTimeListener, and catch-all subscriptions usingMATCH_ALL.
Frequently Asked Questions
What is the EventBus in Home Assistant?
The EventBus is a centralized pub/sub messaging system implemented in homeassistant/core.py that allows components to communicate by firing events and registering listeners. It maintains a thread-safe registry of callbacks and ensures all listeners execute asynchronously without blocking the core event loop.
How do I fire an event from a custom integration?
Use hass.bus.async_fire(event_type, data) when running inside the event loop, or hass.bus.fire(event_type, data) when calling from a background thread. Both methods create an Event object and dispatch it to registered listeners through the internal async_fire_internal pipeline.
Are event listeners thread-safe?
Yes. The EventBus uses HassJob wrappers and async_run_hass_job to execute all listener callbacks in Home Assistant’s executor thread pool. This guarantees that user-provided callback code never blocks the async event loop, regardless of which thread registered the listener.
What is the difference between async_listen and async_listen_once?
async_listen registers a persistent callback that receives every occurrence of an event type until manually removed, while async_listen_once uses the _OneTimeListener wrapper to automatically unregister the callback after it fires the first time, making it ideal for initialization or one-off synchronization tasks.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →