# How Music Assistant Handles Real-Time Updates and Events: Event Bus Architecture Explained

> Learn how Music Assistant delivers real-time updates using event bus architecture and WebSockets. Discover instant synchronization for your music ecosystem.

- Repository: [Music Assistant/server](https://github.com/music-assistant/server)
- Tags: architecture
- Published: 2026-06-15

---

**Music Assistant uses an internal event bus combined with a WebSocket API to push real-time updates to connected clients, enabling instant state synchronization across the ecosystem.**

Music Assistant, the open-source media server (music-assistant/server), implements a robust publish-subscribe mechanism to handle real-time updates and events. The architecture centers on a core event bus that coordinates between internal controllers and external clients through WebSocket connections defined in the server codebase.

## The Core Event Bus Architecture

The `MusicAssistant` class in [`music_assistant/mass.py`](https://github.com/music-assistant/server/blob/main/music_assistant/mass.py) serves as the central nervous system for real-time updates. It maintains a private set of subscribers (`_subscribers`) that receive notifications whenever state changes occur within the system.

### Subscribing to Events

Clients and internal components register interest in specific events using the `subscribe()` method. This method accepts a callable (synchronous or asynchronous), along with optional filters for event types and object IDs.

```python

# music_assistant/mass.py

def subscribe(
    self,
    cb_func: EventCallBackType,
    event_filter: EventType | tuple[EventType, ...] | None = None,
    id_filter: str | tuple[str, ...] | None = None,
) -> Callable[[], None]:
    # … registers the listener in self._subscribers …

```

The method returns an unsubscribe function that removes the callback when invoked.

### Emitting Events with signal_event

When components need to broadcast changes, they invoke `signal_event()` on the `MusicAssistant` instance. This method constructs a `MassEvent` object and forwards it to all matching subscribers.

```python

# music_assistant/mass.py

def signal_event(
    self,
    event: EventType,
    object_id: str | None = None,
    data: Any = None,
) -> None:
    # … builds MassEvent and notifies all subscribers …

```

The `EventType` enum, defined in `music_assistant_models.enums`, enumerates all possible events including `PLAYER_UPDATED`, `PLAYBACK_STARTED`, `PROVIDERS_UPDATED`, and `PLAYER_VOLUME_CHANGED`.

## WebSocket Bridge for Client Delivery

External clients connect via the WebSocket API implemented in [`music_assistant/controllers/webserver/websocket_client.py`](https://github.com/music-assistant/server/blob/main/music_assistant/controllers/webserver/websocket_client.py). The `WebsocketClientHandler` class manages authenticated connections and bridges the internal event bus to the client socket.

### Authentication and Subscription Setup

After successful authentication, the handler registers an internal forwarder callback through `_subscribe_to_events()`. This callback captures all events (or a filtered subset) and pushes them to the connected client.

```python

# music_assistant/controllers/webserver/websocket_client.py

async def _handle_auth_command(self, msg: CommandMessage) -> None:
    # … authentication logic …

    self._subscribe_to_events()          # ← registers the event forwarder

```

```python

# inside WebsocketClientHandler

def _subscribe_to_events(self) -> None:
    # Subscribe to all events (or a subset you care about)

    self._events_unsub_callback = self.mass.subscribe(
        self._send_message_sync,               # forward event to client

        event_filter=None,                     # None = all events

        id_filter=None,
    )

```

### Synchronous Forwarding for Performance

The WebSocket handler uses `_send_message_sync()` rather than asynchronous methods to avoid executor overhead for large JSON payloads. This synchronous write places the encoded `MassEvent` directly into the client's output queue (`_to_write`), ensuring immediate delivery without blocking the main event loop.

## End-to-End Event Flow Example

Consider a volume change scenario:

1. `PlayerController.set_volume()` updates the player state and calls `self.mass.signal_event(EventType.PLAYER_VOLUME_CHANGED, player_id, new_volume)`.
2. The core event bus iterates through `_subscribers` and invokes matching callbacks.
3. The `WebsocketClientHandler` forwarder receives the event and calls `_send_message_sync()`.
4. The client receives a JSON-encoded message:

```json
{
  "event": "PLAYER_UPDATED",
  "object_id": "my_player_id",
  "data": {"state": "playing", "track": "Song A"}
}

```

## Implementation Examples

### Emitting Events from Custom Components

Custom providers or controllers can emit events using the following pattern:

```python

# inside any controller or provider

from music_assistant_models.enums import EventType

def my_update(self):
    # …some state change…

    self.mass.signal_event(
        EventType.CORE_STATE_UPDATED,
        object_id=self.instance_id,
        data={"new_state": "running"},
    )

```

### Programmatic Subscription

Background tasks or providers can subscribe to specific events programmatically:

```python

# Typical usage inside a provider or background task

def on_player_update(event: MassEvent) -> None:
    print(f"Player {event.object_id} changed: {event.data}")

# Register the callback – only PLAYER_UPDATED events for a specific player

unsubscribe = mass.subscribe(
    on_player_update,
    event_filter=(EventType.PLAYER_UPDATED,),
    id_filter=("my_player_id",),
)

# When you no longer need the callback

unsubscribe()

```

## Summary

- **Centralized event bus**: The `MusicAssistant` class in [`music_assistant/mass.py`](https://github.com/music-assistant/server/blob/main/music_assistant/mass.py) manages all real-time updates through `subscribe()` and `signal_event()` methods.
- **Type-safe events**: The `EventType` enum defines all possible events, from player state changes to provider updates.
- **WebSocket integration**: The `WebsocketClientHandler` automatically forwards events to authenticated clients using synchronous message sending for optimal performance.
- **Flexible filtering**: Subscribers can filter by event type and object ID to receive only relevant updates.
- **Thread safety**: The `signal_event()` method validates calls originate from the main asyncio loop, ensuring safe operation across threads.

## Frequently Asked Questions

### How does Music Assistant ensure thread safety when signaling events?

The `signal_event()` method in [`music_assistant/mass.py`](https://github.com/music-assistant/server/blob/main/music_assistant/mass.py) validates that calls originate from the main asyncio loop thread. This safety check prevents race conditions when components running in different threads need to emit events, ensuring the event bus remains consistent.

### What EventType values are available for real-time updates?

The `EventType` enum in [`music_assistant_models/enums.py`](https://github.com/music-assistant/server/blob/main/music_assistant_models/enums.py) defines all available events, including `PLAYER_UPDATED`, `PLAYER_VOLUME_CHANGED`, `PLAYBACK_STARTED`, `PLAYBACK_PAUSED`, `PROVIDERS_UPDATED`, and `CORE_STATE_UPDATED`. Controllers emit these events whenever corresponding state changes occur.

### Can I filter events for specific objects or types?

Yes. The `subscribe()` method accepts `event_filter` and `id_filter` parameters. Pass a single `EventType` or tuple of types to filter events, and provide an object ID or tuple of IDs to restrict updates to specific entities. Pass `None` to receive all events.

### Why does the WebSocket handler use synchronous message sending?

The `WebsocketClientHandler` uses `_send_message_sync()` to forward events because it avoids the overhead of the executor typically used for large JSON payloads. Since event messages are small and the method runs in the request thread, this approach delivers real-time updates immediately to the client's output queue without blocking the async event loop.