Server Push

This section documents the current Vaadin release line — Vaadin 24 LTS / 25.x, Java 17+, Spring Boot 3 / Jakarta EE 10 — as published at the official Vaadin documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. Flow (server-side Java) is the authoring style used throughout, with Hilla / React shown where it differs; Vaadin 7 and the pre-Flow architecture appear only as migration contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vaadin ships major releases roughly twice a year and its ecosystem iterates.

This section’s bibliography lists the reference material consulted while preparing these pages.

By default a Flow UI only changes in response to a user action. Server push keeps a channel open so the server can send DOM updates at any time — for progress bars, dashboards, chat, or notifying one user of another’s change. This page follows Server Push and the Building Apps guide.

Enabling push

Add @Push to the class that implements AppShellConfigurator (there is exactly one per application):

@Push
public class AppShell implements AppShellConfigurator { }

@Push(transport = Transport.WEBSOCKET) is the default; Transport.LONG_POLLING falls back to a series of held-open HTTP requests where a WebSocket cannot be established (some proxies). @Push(PushMode.MANUAL) means you decide when a batch of changes is flushed with ui.push(); the default PushMode.AUTOMATIC flushes after every access block.

The session lock

A VaadinSession and its UIs are not thread-safe. Every read or write of a component from a thread other than the one handling the current request must run inside UI.access(Command), which acquires the session lock, runs the command, and (in automatic mode) pushes the result:

sequenceDiagram participant W as Worker thread participant L as Session lock participant UI as UI / component tree participant B as Browser W->>W: long-running work (query, report, API call) W->>L: ui.access(() -> ...) L-->>W: lock acquired W->>UI: mutate components (progressBar.setValue(...)) W->>L: command returns, lock released L->>B: push DOM diff over the open channel B->>B: apply update
UI ui = UI.getCurrent();     // capture on the request thread, before starting the work

executor.execute(() -> {
    String report = reportService.build();     // slow — off the UI thread
    ui.access(() -> {
        downloadLink.setHref(report);
        Notification.show("Report ready");
    });
});

ui.access(…​) is asynchronous and safe to call from any thread; ui.accessSynchronously(…​) blocks the caller and must never be called from a request thread already holding the lock.

Running background work

Prefer the managed VaadinExecutor — it propagates the Vaadin and (with Spring) security context into the worker thread, and its tasks are cancelled when the UI detaches:

VaadinExecutor executor = VaadinService.getCurrent().getContext()
        .getAttribute(VaadinExecutor.class);

executor.execute(ui, () -> {
    for (int i = 0; i <= 100; i += 10) {
        int pct = i;
        doChunk();
        ui.access(() -> progressBar.setValue(pct / 100.0));
    }
});

A plain ExecutorService or a Spring @Async method works too; you are then responsible for capturing the UI reference and for not leaking threads when the user closes the tab.

Broadcasting to many UIs

For "notify everyone" features, keep a thread-safe registry of listeners and fan out to each UI through its own access(…​):

public final class Broadcaster {
    private static final Set<Consumer<String>> LISTENERS = new CopyOnWriteArraySet<>();

    public static Registration register(Consumer<String> listener) {
        LISTENERS.add(listener);
        return () -> LISTENERS.remove(listener);
    }

    public static void broadcast(String message) {
        LISTENERS.forEach(l -> l.accept(message));
    }
}

// in a view
private Registration registration;

@Override
protected void onAttach(AttachEvent event) {
    UI ui = event.getUI();
    registration = Broadcaster.register(msg ->
            ui.access(() -> Notification.show(msg)));
}

@Override
protected void onDetach(DetachEvent event) {
    registration.remove();   // avoid leaking the listener and the UI
}

An ui.access(…​) scheduled against a UI that has since detached throws UIDetachedException; unregistering in onDetach (as above) prevents it, and wrapping the body in a try/catch handles the race.

The no-push alternative

If push cannot be enabled, ui.setPollInterval(millis) makes the browser poll on a timer; a PollListener runs on each poll and can refresh the UI. It costs a request per interval per open tab, so use push where you can:

ui.setPollInterval(2000);
ui.addPollListener(e -> grid.getDataProvider().refreshAll());

See also