> Markdown version of [Service Event Bus](https://vaadin.com/docs/next/flow/advanced/session-lock-and-rpc-listeners). Section index: [llms.txt](https://vaadin.com/docs/next/flow/llms.txt)

# Service Event Bus (since V25.3)

Every `VaadinService` has an event bus through which the framework reports what it’s doing, and through which an application can fire service-wide events of its own. The events the framework fires cover the machinery that a request passes through: the session lock that serializes all server-side work for a session, the individual client-to-server RPC invocations handled while that lock is held, the data provider queries those invocations trigger, and the server-side navigations that a router link click, a `UI.navigate()` call, or a page load can start. Listening to them is useful for performance monitoring, distributed tracing, and diagnosing lock contention — without modifying application logic.

The bus is reached with `VaadinService.getEventBus()`, and a listener is added for one event type:

```java
Registration registration = service.getEventBus()
        .addListener(SessionLockAcquiredEvent.class,
                event -> handleLockAcquired(event));
```

The returned `Registration` removes the listener again with `remove()`.

A few properties of the bus are worth knowing:

- Events are dispatched by their **exact runtime type**. A listener registered for a supertype — for instance `AbstractDataFetchEvent` — is never notified; register for each concrete event type you want.

- The bus is **thread-safe**. Listeners can be added and removed while events are being fired from other request threads. Requests belonging to different sessions are handled concurrently, so a listener must expect events from several sessions on several threads at once.

- An exception thrown by a listener is **logged, and the remaining listeners are notified regardless**, so that one misbehaving listener can neither disrupt the framework nor hide the event from other listeners.

- Listeners run **on the request or access thread**, directly around the operation they report. Implementations must be fast and non-blocking.

## <a id="registering-listeners"></a>Registering Listeners

Listeners are typically added from a [`VaadinServiceInitListener`](https://vaadin.com/docs/next/flow/advanced/service-init-listener.md), which receives the service before it starts handling requests. How that init listener itself is discovered depends on the project type:

- In **Spring and CDI** projects, annotate the class with `@Component` (Spring) or make it a managed bean (CDI); it’s then registered automatically.

- In **plain Java** projects, register it through the Java Service Provider Interface by listing its fully qualified class name in `META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener`.

See [Service Init Listener](https://vaadin.com/docs/next/flow/advanced/service-init-listener.md) for the full details of each approach.

## <a id="session-lock-events"></a>Session Lock Events

Because all of a session’s server-side work is serialized behind a single lock, the time a thread spends blocked acquiring that lock (the *wait time*) and the time it then holds it (the *hold time*) are key performance signals. Three events expose both:

| Event                       | When It’s Fired                                                                       |
| --------------------------- | ------------------------------------------------------------------------------------- |
| `SessionLockRequestedEvent` | Immediately before the thread attempts to acquire the lock.                           |
| `SessionLockAcquiredEvent`  | Immediately after the lock has been acquired.                                         |
| `SessionLockReleasedEvent`  | Immediately after the outermost hold has been released (the hold count reached zero). |

All three carry the `VaadinService` the lock belongs to, through `getService()`.

The same lock instance protects a session whether it’s acquired by the framework while handling a request, or through `VaadinSession.lock()` — for example from [`UI.access()`](https://vaadin.com/docs/next/flow/advanced/server-push.md). Events are fired for the *outermost* acquisition only; reentrant re-locks aren’t reported. For a single lock-hold, all three are fired on the same thread, in the order requested → acquired → released. This makes a `ThreadLocal` a natural place to record timing, as shown in the following example:

`SessionLockMetricsInitListener.java`

```java
public class SessionLockMetricsInitListener
        implements VaadinServiceInitListener {

    private final ThreadLocal<Long> requestedAt = new ThreadLocal<>();
    private final ThreadLocal<Long> acquiredAt = new ThreadLocal<>();

    @Override
    public void serviceInit(ServiceInitEvent event) {
        VaadinServiceEventBus eventBus = event.getSource().getEventBus();

        eventBus.addListener(SessionLockRequestedEvent.class,
                lockEvent -> requestedAt.set(System.nanoTime()));

        eventBus.addListener(SessionLockAcquiredEvent.class, lockEvent -> {
            long acquired = System.nanoTime();
            acquiredAt.set(acquired);
            LoggerFactory.getLogger(getClass()).debug(
                    "Session lock wait: {} ms",
                    (acquired - requestedAt.get()) / 1_000_000.0);
        });

        eventBus.addListener(SessionLockReleasedEvent.class, lockEvent -> {
            long holdNanos = System.nanoTime() - acquiredAt.get();
            LoggerFactory.getLogger(getClass())
                    .debug("Session lock hold: {} ms", holdNanos / 1_000_000.0);
            requestedAt.remove();
            acquiredAt.remove();
        });
    }
}
```

When several listeners are registered, the requested and acquired events are delivered in registration order, while the released event is delivered in reverse registration order. Listeners therefore nest like `try`/`finally` blocks: a listener registered later sees the release before one registered earlier does.

Listeners registered after a session’s lock has already been created are still honored.

## <a id="rpc-invocation-events"></a>RPC Invocation Events

A single client request typically carries several RPC invocations — a DOM event, a synchronized property update, a `@ClientCallable` or template event handler, a server-side navigation, a return channel message, and so on. One event of each type is fired per invocation, which is useful for emitting a tracing span that shows exactly which invocation consumes the time spent holding the session lock during a request.

| Event                       | When It’s Fired                                                                                                                                                                                                                                                                                                 |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RpcInvocationStartedEvent` | Immediately before an invocation is handled.                                                                                                                                                                                                                                                                    |
| `RpcInvocationFailedEvent`  | When handling an invocation threw, before the ended event. The throwable is available from `getError()`. The framework still routes it to the session error handler independently of this event.                                                                                                                |
| `RpcInvocationEndedEvent`   | Once an invocation has been handled, whether it completed normally or threw. `getDuration()` gives how long it took, measured from just before the started event was fired. `getError()` gives the throwable it raised — the same one the failed event carried — or an empty optional if it completed normally. |

All three expose the same details about the invocation:

| Method           | Description                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getUI()`        | The `UI` the invocation is handled against. Never `null`.                                                                                                                                                                                                                                                                                                                                                                             |
| `getType()`      | The protocol-level invocation type, such as `event`, `mSync`, `publishedEventHandler`, `navigation`, or `channel`. Never `null`.                                                                                                                                                                                                                                                                                                      |
| `getNodeId()`    | The id of the targeted `StateNode`, or `-1` if the invocation doesn’t target a node.                                                                                                                                                                                                                                                                                                                                                  |
| `getName()`      | A human-readable identifier — the DOM event name, the invoked method name, the navigation location, and so on — or `null` if none applies. It never carries the data of the invocation, only its identity.                                                                                                                                                                                                                            |
| `getComponent()` | An `Optional` with the component the invocation targets, looked up in the state tree each time the method is called. Empty if the invocation doesn’t target a node, the node is no longer in the UI, or the node isn’t the element of a component. Call it while the invocation is being handled — if the invocation detaches the component it targets, the component is found for the started event but no longer for the ended one. |

For one invocation, the started event, the optional failed event, and the ended event are fired on the same thread, in that order. The ended event is always fired after the started one, regardless of outcome, so a listener may keep state in a `ThreadLocal`. The ended event carries what such state is most often kept for — the duration and the error of the invocation — so a listener that only needs those can listen for the ended event alone. Within one request the events don’t nest: those of one invocation are all fired before those of the next.

Being reported doesn’t mean the invocation had an effect: an RPC targeting a node that’s detached, disabled, or inert is reported and only then discarded unhandled.

## <a id="data-provider-query-events"></a>Data Provider Query Events

Every fetch and count query a data-bound component issues is reported on the bus, so that slow backend queries can be measured where they happen instead of being inferred from overall request timings.

| Event                   | When It’s Fired                                                                                                                                                                                                                                                                                                  |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DataFetchStartedEvent` | Before a page of items is requested from a data provider.                                                                                                                                                                                                                                                        |
| `DataFetchFailedEvent`  | When the fetch threw, before the ended event. The throwable is available from `getError()`.                                                                                                                                                                                                                      |
| `DataFetchEndedEvent`   | Once the items have been loaded and consumed, or the fetch threw. `getRowsReturned()` gives the number of items the data provider actually returned — possibly fewer than requested — or `-1` if it threw. `getDuration()` gives how long the fetch took, measured from just before the started event was fired. |
| `DataCountStartedEvent` | Before a count query is issued.                                                                                                                                                                                                                                                                                  |
| `DataCountFailedEvent`  | When the count query threw, before the ended event.                                                                                                                                                                                                                                                              |
| `DataCountEndedEvent`   | Once the count query has returned or thrown. `getCount()` gives the reported number of items, or `-1` if it threw. `getDuration()` gives how long the query took, measured from just before the started event was fired.                                                                                         |

The events describe the query and where it came from:

| Method                         | Description                                                                                                                                              |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getUI()`                      | The `UI` the component belongs to. Never `null`.                                                                                                         |
| `getComponent()`               | An `Optional` with the component whose data is being loaded, so that a query can be attributed to a view. Empty when the component couldn’t be resolved. |
| `getOffset()` and `getLimit()` | The index of the first item requested, and how many were requested. Fetch events only.                                                                   |
| `isFiltered()`                 | Whether the query carried a filter, which distinguishes, for example, a Combo Box search from its initial page load.                                     |

Because a data provider may return a lazily evaluated `Stream`, the ended event is fired only after the returned items have been consumed, so the measured duration covers the backend round-trip rather than only the call that started it. The started and ended events of a query are fired on the same thread, and the ended event is fired in reverse registration order, so listeners nest around the started one. Queries made for a component that isn’t attached to a live UI aren’t reported.

> **Note:** Data fetches triggered by push updates run on the executor given to `DataCommunicator.enablePushUpdates()`, so these events aren’t always fired on a request thread.

## <a id="navigation-events"></a>Navigation Events

Each server-side navigation the router handles fires a matching pair of events, useful for timing navigations and tagging them with their outcome — something that pairing [`BeforeEnterEvent`](https://vaadin.com/docs/next/flow/routing/lifecycle.md#BeforeEnterEvent) and [`AfterNavigationEvent`](https://vaadin.com/docs/next/flow/routing/lifecycle.md#AfterNavigationEvent) listeners gets wrong for forwards, reroutes, redirects, postponed navigations, and error views.

| Event                    | When It’s Fired                                                                                                    |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| `NavigationStartedEvent` | Before the router starts handling a navigation, before any `BeforeLeaveEvent` or `BeforeEnterEvent` listener runs. |
| `NavigationEndedEvent`   | Once the navigation has finished, whatever the outcome.                                                            |

Only the **outermost** navigation is reported. A [reroute](https://vaadin.com/docs/next/flow/routing/lifecycle.md#Rerouting), a [forward](https://vaadin.com/docs/next/flow/routing/lifecycle.md#Forwarding), the redirect that adds or removes a trailing slash, and the rendering of an error view are part of the navigation that caused them and fire no events of their own; their effect shows up in the ended event’s outcome instead. Two cases fire no events at all, since they continue a navigation that has already been reported: resuming a [postponed](https://vaadin.com/docs/next/flow/routing/lifecycle.md#postpone) navigation, and showing a `PreserveOnRefresh` view once the browser has sent its window name.

Both events carry the details of the navigation that was requested:

| Method          | Description                                                                                                                     |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `getUI()`       | The `UI` that navigates. Never `null`.                                                                                          |
| `getLocation()` | The requested location, before any forward or reroute. Never `null`.                                                            |
| `getTrigger()`  | The action that triggered the navigation, such as a page load, a router link click, or a call to `UI.navigate()`. Never `null`. |

`NavigationEndedEvent` additionally exposes how the navigation ended:

| Method            | Description                                                                                                                                                                                                                                                                                                                                                                                     |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getOutcome()`    | One of the records of the sealed `NavigationEndedEvent.Outcome` interface: `Completed` (the view that’s shown after all forwards and reroutes), `Postponed` (a leave listener postponed the navigation), `Failed` (an error view was shown, or the navigation threw), or `NotShown` (no server view was shown, for example for a client-side route the browser renders or an external forward). |
| `getStatusCode()` | The HTTP status code of the navigation, such as 200 or 404, or `-1` if the navigation threw.                                                                                                                                                                                                                                                                                                    |

For one navigation, the started event and the ended event are fired on the same thread, in that order, even when the navigation throws, so timing state can again be kept in a `ThreadLocal`. The ended event is fired in reverse registration order, so listeners nest around the started one.

**Example**: Finding broken links in the application.

A navigation that ends with status 404 after the user opened a URL directly usually means a mistyped address or an outdated bookmark. When the user followed a router link instead, the application itself links to a route that doesn’t exist:

```java
service.getEventBus().addListener(NavigationEndedEvent.class, event -> {
    if (event.getStatusCode() == 404
            && event.getTrigger() == NavigationTrigger.ROUTER_LINK) {
        LoggerFactory.getLogger(getClass()).warn(
                "Router link points to a missing route: {}",
                event.getLocation().getPath());
    }
});
```

## <a id="other-framework-events"></a>Other Framework Events

The service lifecycle events are fired through the same bus, and the dedicated `addSessionInitListener()`, `addSessionDestroyListener()`, `addServiceDestroyListener()`, and `addUIInitListener()` methods on `VaadinService` are thin wrappers that register on it. Either style works: use the listener interface when it exists, or listen for `SessionInitEvent`, `SessionDestroyEvent`, `ServiceDestroyEvent`, and `UIInitEvent` directly on the bus.

## <a id="firing-your-own-events"></a>Firing Your Own Events

Any component of an application that needs to notify service-wide listeners can define an event type of its own and fire it on the bus, instead of maintaining its own listener collection. An event type only has to extend `EventObject`:

```java
public class MaintenanceModeEvent extends EventObject {
    public MaintenanceModeEvent(VaadinService service) {
        super(service);
    }
}

// Elsewhere, to notify the listeners:
service.getEventBus().fireEvent(new MaintenanceModeEvent(service));
```

Three firing methods are available:

| Method                           | Description                                                                                                                                           |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fireEvent(event)`               | Notifies the listeners of the event type in registration order, logging an exception from a listener and continuing with the rest.                    |
| `fireEvent(event, errorHandler)` | The same, but hands a listener that threw to the given error handler instead of logging it.                                                           |
| `fireEventInReverseOrder(event)` | Notifies the listeners in reverse registration order. Use it for the closing half of a pair of events, so that listeners nest around the opening one. |

On a hot code path, `hasListener(MaintenanceModeEvent.class)` tells whether building the event is worth it at all.

## <a id="deprecated-listener-interfaces"></a>Deprecated Listener Interfaces (deprecated since V25.3)

`SessionLockListener` and `RpcInvocationListener`, together with the `VaadinService.addSessionLockListener()` and `VaadinService.addRpcInvocationListener()` methods that register them, are deprecated for removal. They still work and are still delivered from the bus, but new code should listen for the events directly:

| Deprecated Callback                         | Event                       |
| ------------------------------------------- | --------------------------- |
| `SessionLockListener.lockRequested()`       | `SessionLockRequestedEvent` |
| `SessionLockListener.lockAcquired()`        | `SessionLockAcquiredEvent`  |
| `SessionLockListener.lockReleased()`        | `SessionLockReleasedEvent`  |
| `RpcInvocationListener.invocationStarted()` | `RpcInvocationStartedEvent` |
| `RpcInvocationListener.invocationFailed()`  | `RpcInvocationFailedEvent`  |
| `RpcInvocationListener.invocationEnded()`   | `RpcInvocationEndedEvent`   |

The `SessionLockEvent` and `RpcInvocationEvent` classes the callbacks receive are deprecated with them; the new events carry the same information.

`970a4b45-bc87-4381-9ab9-7c3e34e97b26`
