WebSocket Push with f:websocket

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Jakarta Faces 4.1 (API jakarta.faces-api 4.1.2, part of Jakarta EE 11, which lists Jakarta WebSocket 2.2 as an optional dependency of the Faces specification), Eclipse Mojarra 4.1.17 and Apache MyFaces Core 4.1.4, Java 17 or later (the examples were compiled with --release 21), Jakarta Servlet 6.1 and Jakarta WebSocket 2.2. f:websocket and @Push exist since JSF 2.3, so almost everything below also applies to Faces 4.0; the differences are collected in What Changed by Version. Every example was deployed in a scratch Bookshelf WAR on Apache Tomcat 11.0.26 with Weld 6.0.4.Final, once with Mojarra and once with MyFaces. Pushes were received by scripted WebSocket clients (Node.js 24) and by a headless Chrome page driven through the DevTools protocol; every message, close code and error text quoted below was observed in those runs on 2026-10-08. Claims taken only from the documentation are marked as such.

An HTTP request is always started by the browser. A page that has to show "a new review just arrived" has three options: ask again and again (polling), keep one request open (long polling or Server-Sent Events), or open a WebSocket, a long-lived full-duplex connection defined by RFC 6455 on which the server can send a frame whenever it wants. Faces wraps the WebSocket option in one tag, f:websocket, and one injectable object, PushContext. The design is deliberately small: the channel is one-way (server to client), and everything the browser has to send back goes through ordinary Faces Ajax, which keeps the view state, the HTTP session and the security constraints that a raw WebSocket message would not carry (see the partial lifecycle).

How It Works

sequenceDiagram autonumber participant B as Browser participant F as FacesServlet participant E as Push endpoint participant S as ReviewService and LiveReviews B->>F: GET /live.xhtml F-->>B: HTML with the faces.push.init script (URL = channel plus generated id) B->>E: WebSocket handshake to /jakarta.faces.push/reviews (session cookie) E-->>B: 101 Switching Protocols, onopen runs B->>F: Ajax POST, a new review F->>S: ReviewService.save, CDI event ReviewSaved S->>E: PushContext.send(message) encodes JSON E-->>B: text frame, onmessage runs Note over B,E: the connection stays open and reconnects by itself

The pieces, in the order they appear:

  • The context parameter jakarta.faces.ENABLE_WEBSOCKET_ENDPOINT registers the server endpoint (Enabling the Endpoint).

  • The tag f:websocket creates a jakarta.faces.component.UIWebsocket component. It renders no markup, only a <script> that calls faces.push.init(...) with a URL of the form /jakarta.faces.push/<channel>?<generated id> (The f:websocket Tag).

  • The JavaScript API faces.push in faces.js opens the socket, reconnects it, decodes the JSON and calls your listeners (The Client Side).

  • The CDI side: @Push PushContext injected anywhere sends a message to every socket open on that channel (The Server Side: @Push and PushContext).

  • Scopes and users decide who receives the message (Scopes and Users).

Enabling the Endpoint

The endpoint is off by default. Enable it in web.xml:

WEB-INF/web.xml (excerpt)
<context-param>
    <param-name>jakarta.faces.ENABLE_WEBSOCKET_ENDPOINT</param-name>
    <param-value>true</param-value>
</context-param>

The constant is PushContext.ENABLE_WEBSOCKET_ENDPOINT_PARAM_NAME. If the servlet container runs its WebSocket container on a different TCP port than HTTP, the optional integer parameter jakarta.faces.WEBSOCKET_ENDPOINT_PORT (PushContext.WEBSOCKET_ENDPOINT_PORT_PARAM_NAME) tells Faces which port to put in the generated URL:

<context-param>
    <param-name>jakarta.faces.WEBSOCKET_ENDPOINT_PORT</param-name>
    <param-value>8000</param-value>
</context-param>

What happens when it is missing differs between the implementations (both observed):

Implementation Result of a page with f:websocket and no ENABLE_WEBSOCKET_ENDPOINT

Mojarra 4.1.17

The page fails at render time with the error f:websocket endpoint is not enabled. You need to set web.xml context param 'jakarta.faces.ENABLE_WEBSOCKET_ENDPOINT' with value 'true'., which is the IllegalStateException that the UIWebsocket constructor is documented to throw.

MyFaces 4.1.4

The page renders normally and contains the faces.push.init script, but the browser’s connection attempt fails (a Node.js client saw close code 1006 and no onopen). Nothing in the page tells you why, so check the context parameter first when a socket never opens.

Prerequisites by runtime. A server that implements the platform (WildFly, GlassFish, Payara, Open Liberty, TomEE in their full configurations) provides the WebSocket container and JSON-P, so only the parameter is needed. Check that the server’s profile actually includes Jakarta WebSocket and JSON-P. Tomcat has the WebSocket container but not JSON-P, and Mojarra encodes every message with Jakarta JSON Processing. On Tomcat 11.0.26 plus Weld with Mojarra 4.1.17 the first PushContext.send(...) failed with java.lang.NoClassDefFoundError: jakarta/json/Json (thrown in the sender, the page itself rendered fine; the frame that a client receives once the jars are present is {"data":...}) until jakarta.json:jakarta.json-api and an implementation (org.eclipse.parsson:parsson) were added to WEB-INF/lib. MyFaces 4.1.4 on the same Tomcat pushed without jakarta.json or Parsson in the WAR; the frame it sends is a bare JSON string (the encoded message itself, without the data wrapper that Mojarra’s frame has). So the two implementations differ here: Mojarra needs JSON-P on Tomcat or Jetty, MyFaces did not in this test, and a WAR that must run on both, or that may be switched from one to the other, should carry JSON-P anyway. A client that parses the frame itself, rather than through faces.push, has to cope with both frame shapes. Both observations are from one run each on that Tomcat. See Tomcat and Weld and Jakarta EE servers for the rest of such a setup; the Maven WAR setup lists the baseline dependencies.

<dependency>
    <groupId>jakarta.json</groupId>
    <artifactId>jakarta.json-api</artifactId>
    <version>2.1.3</version>
</dependency>
<dependency>
    <groupId>org.eclipse.parsson</groupId>
    <artifactId>parsson</artifactId>
    <version>1.1.7</version>
</dependency>

Source: Jakarta Faces 4.1 specification (optional Jakarta WebSocket dependency) and Jakarta WebSocket 2.2

The f:websocket Tag

f:websocket is in the jakarta.faces.core namespace (xmlns:f="jakarta.faces.core"). Component type jakarta.faces.Websocket, class jakarta.faces.component.UIWebsocket (a ClientBehaviorHolder, which is what lets a nested f:ajax listen to the pushed message, see Updating the UI from a Push: f:ajax and h:commandScript), renderer com.sun.faces.renderkit.html_basic.WebsocketRenderer in Mojarra. Attributes, as documented in the 4.1 VDL:

Attribute Required Meaning

channel

yes

The channel name. It may not be an EL expression and may only contain alphanumeric characters, hyphens, underscores and periods. All open sockets on the same channel (and, for session/view scope, the same session/view) receive the same message.

scope

no

application (the default), session or view, case insensitive, not an EL expression. With user set the default is session (Scopes and Users).

user

no

A Serializable identifier of the logged-in user, so that messages can be addressed to one user. Keep it small; the documentation suggests #{request.remoteUser} or #{someLoggedInUser.id}.

onopen

no

JavaScript function called when the socket opens. Argument: the channel name.

onmessage

no

JavaScript function called for every message. Arguments: the decoded message, the channel name, the raw MessageEvent.

onerror

no

Called when a connection error occurred and the socket will try to reconnect. Arguments: the close code, the channel name, the raw CloseEvent. Added in Faces 4.0.

onclose

no

Called when the socket is closed and will not reconnect. Arguments: the close code, the channel name, the raw CloseEvent.

connected

no

Boolean, default true. Whether to connect. May be an EL expression that is re-evaluated on every Ajax request (Controlling the Connection: connected, rendered and faces.push).

rendered

no

Boolean, default true. Whether to render the script at all. Also re-evaluated on Ajax requests; turning it false closes an open socket.

id, binding

no

Standard component identifier and component binding. The component’s client id is what faces.push.open and faces.push.close take.

UIWebsocket rejects invalid configuration when the page is rendered. Observed:

Mistake Mojarra 4.1.17 MyFaces 4.1.4

channel="#{param.c}" (EL in channel)

Error page whose message is just channel (the IllegalArgumentException that setValueExpression is documented to throw)

Same

channel="my channel" (space)

f:websocket 'channel' attribute 'my channel' does not represent a valid channel name. It is required, it may not be an Jakarta Expression Language expression and it may only contain alphanumeric characters, hyphens, underscores and periods.

Accepted; renders a URL containing the space. Use only the allowed characters anyway.

scope="request"

f:websocket 'scope' attribute 'request' does not represent a valid scope...

Scope 'request' not supported!

scope="application" together with user

Rejected with the invalid-scope message, which ends with the hint that with user the scope defaults to session and may not be application

Accepted; the page rendered with a plain init script

The same channel name used with two scopes (for example application on one page, session on another)

f:websocket channel 'alerts' is already registered on a different scope. Choose an unique channel name for a different channel... (also across sessions, until the JVM restarts)

No error observed

The Client Side

Listeners

The listener attributes take the name of a function (without parentheses) or an inline function expression:

<f:websocket channel="reviews" onmessage="showReview"/>
<f:websocket channel="reviews" onmessage="function (review) { console.log(review); }"/>

The documented life cycle of a socket:

  • The socket is opened when the document loads and stays open as long as the document is open. It is closed implicitly when the document is unloaded.

  • When a successfully connected socket is broken (network error, server restart) it reconnects automatically at increasing intervals. It does not reconnect when the very first connection attempt fails.

  • onopen runs on the very first connection attempt, whether or not it succeeds, and not again after an automatic reconnect.

  • onerror runs on every failed connection of an already established socket, while it still intends to reconnect. It is not called on the final close.

  • onclose runs when the socket will not reconnect: the first attempt failed, the server closed it with 1000 (normal closure) or 1008 (policy violated), or the maximum number of reconnects was exceeded. The code -1 means the browser has no WebSocket support, 1000 a normal close (the session or view expired), anything else an error. See RFC 6455 section 7.4.1 and CloseReason.CloseCodes.

Observed with listeners that logged into the page: onopen fired once, faces.push.close(...) produced onclose with code 1000, and invalidating the HTTP session produced a server-side close with code 1000 on a view-scoped channel (Mojarra sent the reason text Expired), on Mojarra and on MyFaces. The reconnect intervals and the server-restart behaviour were not run; they come from the documentation.

The 4.1 JavaScript documentation of faces.push.init says onopen receives "the client identifier", while the VDL says "the channel name". In both implementations onopen received the channel name (OPEN reviews in the log of the test page), as the VDL says.

Define the Listeners Before the Tag

f:websocket renders its faces.push.init(...) call inline, at the place where the tag stands, and passes the listener functions by reference. A listener defined in a <script> after the tag does not exist yet when that call runs:

Wrong order: the listener functions are declared below the tag
<f:websocket channel="reviews" onopen="onOpen" onmessage="showReview"/>
<script>
    function onOpen(channel) { /* ... */ }
    function showReview(review) { /* ... */ }
</script>

On Mojarra this page logged ReferenceError: onOpen is not defined and no socket was initialised. MyFaces rendered the same page without error (it resolved the functions later). Put the listeners in <h:head>, in a script resource loaded with h:outputScript, or before the tag, as the Bookshelf page below does. The same ordering rule applies to the name of an h:commandScript used as listener (Updating the UI from a Push: f:ajax and h:commandScript).

The Message Argument

The message is the Java object you passed to send, encoded as JSON and decoded by the script, so the listener receives a JavaScript string, number, array or object. Always treat it as data, never as markup: write it with textContent (or a template that escapes), not with innerHTML, because review text is entered by users. The Bookshelf listener below passes the comment <b>Great</b> read through a push and it showed as literal text on both implementations. See Security.

The Server Side: @Push and PushContext

jakarta.faces.push.Push is a CDI qualifier (@Qualifier, targets method, field and parameter) and jakarta.faces.push.PushContext the injectable object. Inject it into any container-managed artefact, a managed bean, a servlet, an observer or an EJB:

@Inject
@Push
private PushContext reviews;          // channel name = the field name: "reviews"

@Inject
@Push(channel = "alerts")             // channel name given explicitly
private PushContext notifications;

The channel name is taken from the name of the injection target unless channel is given. This is the most common mistake: a field called reviewsChannel sends to a channel reviewsChannel, nobody listens on it and nothing happens and nothing fails. It happened in the scratch app (the page listened on reviews, the push went into the void with HTTP 200 and no log line). Name the field after the channel or always write @Push(channel = "...").

PushContext (a Serializable interface) has three send methods, each returning Future<Void> handles for the individual socket writes, which you can ignore or use to detect delivery problems:

Method Meaning

Set<Future<Void>> send(Object message)

To every socket open on the channel (within the scope rules).

<S extends Serializable> Set<Future<Void>> send(Object message, S user)

To the sockets whose f:websocket user equals user.

<S extends Serializable> Map<S, Set<Future<Void>>> send(Object message, Collection<S> users)

To several users; the map holds the futures per user.

The Message Is Encoded as JSON

The object is converted by a small JSON encoder inside the implementation. Supported shapes (all verified on both): String, numbers, Boolean, null, arrays, collections, maps and JavaBeans (public getters). Because the encoder differs, so do the results for less common types:

Message content Mojarra 4.1.17 MyFaces 4.1.4

Envelope on the wire

{"data": <your object>}; the script unwraps data before calling onmessage

The JSON of the object itself

JavaBean with getters

Object, properties sorted alphabetically; </b> and é are written as they are

Object with the same property order, but </b> is written <\/b> and é as a \u00e9 escape (the browser decodes both identically)

java.util.Date

ISO-8601 UTC string, "1970-01-01T00:00:00Z"

RFC 1123 string, "Thu, 01 Jan 1970 00:00:00 GMT"

LocalDate, Instant

ISO strings (LocalDate becomes midnight UTC)

A nested object of bean properties

LocalDateTime, ZonedDateTime, any enum

send throws IllegalArgumentException: Cannot invoke getter of property ... (500 in the sending request); nothing is delivered

A nested object of bean properties (an enum became {"declaringClass":...,"value":1})

Character

"x"

{}

A record (accessors title(), not getTitle())

{} (silently empty)

{isbn:"1",reviewer:"Rec",...} with unquoted keys, which is not JSON: the client’s JSON.parse threw SyntaxError and onmessage never ran

The portable rule is therefore: send a String, a number, a Map<String, Object> of those, or a small JavaBean whose getters return only strings, numbers and booleans, and format dates and enums yourself (toString() or an ISO formatter). Do not send entities, records or anything with a long getter graph: besides the failures above, a bean’s whole property graph ends up on the wire, including fields the user must not see.

Bookshelf: A Live Review Feed

The catalogue shows a review form. Every new review should appear at once on every open catalogue page, without reloading. Four small classes and one page do it. The message type is a JavaBean (not a record, see above):

model/ReviewPush.java
package org.example.bookshelf.model;

import java.io.Serializable;

/** What the browser receives: a plain JavaBean with getters, so Faces can turn it into JSON. */
public class ReviewPush implements Serializable {

    private final String title;
    private final String reviewer;
    private final int rating;
    private final String comment;

    public ReviewPush(Review review) {
        this.title = review.title();
        this.reviewer = review.reviewer();
        this.rating = review.rating();
        this.comment = review.comment();
    }

    public String getTitle() { return title; }
    public String getReviewer() { return reviewer; }
    public int getRating() { return rating; }
    public String getComment() { return comment; }
}

The service stores the review and announces it with a CDI event; it knows nothing about WebSockets (see CDI events in backing beans):

service/ReviewService.java
@ApplicationScoped
public class ReviewService {

    private final List<Review> reviews = new CopyOnWriteArrayList<>();

    @Inject
    private Event<ReviewSaved> saved;

    public void save(Review review) {
        reviews.add(review);          // in a real application: persist inside a transaction
        saved.fire(new ReviewSaved(review));
    }

    public List<Review> findAll() {
        return List.copyOf(reviews);
    }
}

public record ReviewSaved(Review review) { }

One observer turns the event into a push. Observing with TransactionPhase.AFTER_SUCCESS pushes only after a surrounding transaction commits, so no page shows a review that was rolled back; when no transaction is active the CDI observer is called immediately, which is what happened on Tomcat:

web/LiveReviews.java
@ApplicationScoped
public class LiveReviews {

    @Inject
    @Push(channel = "reviews")
    private PushContext reviews;

    public void onSaved(@Observes(during = TransactionPhase.AFTER_SUCCESS) ReviewSaved event) {
        reviews.send(new ReviewPush(event.review()));
    }
}

The form’s backing bean only saves (the @Push injection stays out of the UI layer):

web/ReviewForm.java
@Named
@RequestScoped
public class ReviewForm {

    private String reviewer;
    private int rating = 5;
    private String comment;

    @Inject
    private ReviewService service;

    public String submit() {
        service.save(new Review("978-0-13-468599-1", "Effective Java", reviewer, rating, comment));
        comment = null;
        return null;
    }

    // getters and setters omitted
}

And the page. The two functions are declared in <h:head>, before the tag. The listeners use textContent:

live.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core">
<h:head>
    <title>Reviews, live</title>
    <script>
        function showReview(review) {
            const item = document.createElement("li");
            item.textContent = review.reviewer + " rated " + review.title + " "
                    + review.rating + "/5: " + review.comment;
            document.getElementById("live-reviews").prepend(item);
        }
        function liveStatus(text) {
            document.getElementById("live-status").textContent = text;
        }
    </script>
</h:head>
<h:body>
    <h1>Reviews, live</h1>
    <p id="live-status">connecting...</p>
    <ul id="live-reviews"></ul>

    <h:form id="reviewForm">
        <h:messages/>
        <h:inputText id="reviewer" value="#{reviewForm.reviewer}" required="true"/>
        <h:inputTextarea id="comment" value="#{reviewForm.comment}" required="true"/>
        <h:commandButton id="post" value="Post review" action="#{reviewForm.submit}">
            <f:ajax execute="@form" render="@form"/>
        </h:commandButton>
    </h:form>

    <f:websocket channel="reviews"
                 onopen="function () { liveStatus('live'); }"
                 onmessage="showReview"
                 onerror="function (code) { liveStatus('reconnecting (' + code + ')'); }"
                 onclose="function (code) { liveStatus('offline (' + code + ')'); }"/>
</h:body>
</html>

In the headless Chrome run the page showed live after loading; posting the form with the comment <b>Great</b> read added <li>Ana rated Effective Java 5/5: &lt;b&gt;Great&lt;/b&gt; read</li> to the list through the push (not through the Ajax response, which renders only the form) on Mojarra and on MyFaces. Open the page in two windows and the second one gets the review as well. Because the channel is application scoped (the default), everybody with the page open receives it, signed in or not. That is right for a public catalogue and wrong for anything private (Scopes and Users).

Two practical details seen in the runs:

  • The server registers a socket slightly after the browser reports it open. A message sent in the same millisecond as onopen (for example a test that sends from inside the onopen listener) was lost on both implementations; waiting half a second made it arrive. A push that must not be missed needs a state snapshot when the page loads, and live pushes only for what happens afterwards: render the existing reviews from the database in the page, push only new ones.

  • Push is not storage. A client that was offline misses messages; reconnecting does not replay them. The database is the source of truth.

Scopes and Users

A channel’s scope decides which sockets it addresses and who may send:

scope Receivers Who can send

application (default)

Every socket open on the channel name anywhere in the application, in any session.

Anything with the PushContext: the application itself, any user, a timer or a servlet.

session

The sockets on the channel name within the current HTTP session (all tabs of one user).

Code running for that user. Per the documentation, not the application.

view

Only the socket of the current view; another tab on the same URL does not receive it.

Code running for that view, typically an action or Ajax listener in the same view. Not the application.

user="..." (scope defaults to session, may not be application)

The sockets of that user, found by the user identifier, whichever session they are in.

Anything: other users, the application, background jobs.

Session and View Scope Bind to the Injection Context

For session and view scope Mojarra resolves "the current session" and "the current view" when the PushContext is created, and keeps the result. A request-scoped bean that is injected and used inside a Faces request therefore sends to the right view. The same injection in an @ApplicationScoped bean is created once and stays bound to whatever session or view happened to be current at that moment. Observed on a fresh Mojarra server with a view-scoped channel: the first user’s click on a button calling an application-scoped sender updated that user’s page; the second user’s click, in another session, did not (the Ajax listener on the second page never ran). MyFaces delivered both. The cause is visible in the Mojarra source: WebsocketPushContext stores the session and view channel maps in its constructor. So:

  • use application scope or user-targeted messages (below) from application-scoped beans and background jobs;

  • for session and view scope inject the PushContext into a bean with a matching or shorter life (request, view, session) and send while that request is running.

The PushContext of a view-scoped channel injected into a request-scoped bean and called from an Ajax listener worked on both implementations (the push arrived in the same view, the nested f:ajax below ran).

Addressing Users

Set user to the authenticated user’s name and send with the user as second argument:

alerts.xhtml
<f:websocket channel="alerts" user="#{request.remoteUser}"
             onmessage="function (text) { showAlert(text); }"/>
Sending to one user, or to several
@ApplicationScoped
public class Alerts {

    @Inject
    @Push
    private PushContext alerts;

    public void notifyUser(String text, String userName) {
        alerts.send(text, userName);
    }

    public void notifyUsers(String text, Collection<String> userNames) {
        alerts.send(text, userNames);
    }
}

With three clients registered as alice, bob and carol, send(text, "alice") reached only alice’s socket, and send(text, List.of("alice", "carol")) reached those two, on both implementations. A plain send(text) on that channel reached nobody: once a channel is declared with user, it has no application-wide audience.

Where the user value comes from matters. The scratch page took the user from a request parameter to be able to simulate three users; with that page, anybody can open ?u=alice and receive alice’s messages. In a real page the value must be the authenticated identity (#{request.remoteUser}, the Jakarta Security principal name, or a server-side user id), never anything the browser can choose. Do not put an entire user entity there: the value must be Serializable and small. See the implicit objects for request.

The OmniFaces documentation adds a caveat for session-scoped user sockets: if the application lets the logged-in user change without invalidating the session, a reused socket can still address the previous user. Invalidate the session on logout, which is good practice anyway.

Updating the UI from a Push: f:ajax and h:commandScript

Writing DOM code in onmessage is fine for a list of lines but awkward when the new data should flow through the component tree, with converters, formatting and templates. Two documented patterns let the push trigger an ordinary Faces Ajax request that re-renders components.

Nested f:ajax. UIWebsocket is a client behavior holder that accepts any event name, so the message string is the event name. Send the string, and the f:ajax whose event equals it fires:

live-count.xhtml
<h:panelGroup id="count" layout="block">
    <h:outputText value="Reviews so far: #{liveCount.total}"/>
</h:panelGroup>

<h:form>
    <f:websocket channel="reviewCount" scope="view">
        <f:ajax event="reviewAdded" listener="#{liveCount.refresh}" render=":count"/>
    </f:websocket>
</h:form>
The listener and the sender
@Named
@RequestScoped
public class LiveCount {

    @Inject
    private ReviewService service;

    @Inject
    @Push(channel = "reviewCount")
    private PushContext channel;

    public int getTotal() {
        return service.findAll().size();
    }

    public void refresh(AjaxBehaviorEvent event) {
        // runs as a normal Faces Ajax request; nothing to do, the render attribute re-reads getTotal()
    }

    public void announce() {
        channel.send("reviewAdded");   // called from an action in the same view
    }
}

Declare the listener method with an AjaxBehaviorEvent parameter. With a no-argument method Mojarra worked but MyFaces 4.1.4 on Tomcat failed in Invoke Application with MethodNotFoundException: Method not found: ...ping(jakarta.faces.event.AjaxBehaviorEvent). Both nested-f:ajax runs updated the page when the method took the event.

h:commandScript. h:commandScript (Faces 2.3) generates a JavaScript function that fires a Faces Ajax request with parameters. Use its name as the onmessage listener. When the message is a Map or a JavaBean, its entries are posted as request parameters:

live-script.xhtml
<h:form id="g">
    <h:commandScript name="refreshReviews" action="#{liveScript.refresh}" render=":list"/>
</h:form>
<h:panelGroup id="list" layout="block">...</h:panelGroup>

<f:websocket channel="reviewScript" scope="view" onmessage="refreshReviews"/>

The h:commandScript form is declared before the tag so the function exists when the tag’s script runs (The Client Side). With the message Map.of("isbn", "978-0", "count", 3) the action’s request contained isbn=978-0 and count=3 in getRequestParameterMap() on both implementations, next to the usual jakarta.faces.partial.* parameters.

Both patterns run the full Faces lifecycle for the Ajax request (partial lifecycle), including restore view and, with execute, validation. They are heavier than a DOM update and fire once per message: for a chatty channel send one summary message instead of fifty, or coalesce on the server.

Controlling the Connection: connected, rendered and faces.push

By default the socket opens when the page loads. Three ways to change that:

  • Do not connect until needed. connected="false" renders the script but does not open the socket. Open it from JavaScript with faces.push.open(clientId) and close it with faces.push.close(clientId), where clientId is the component’s client id (give the tag an id). The functions throw if the client id is unknown.

  • Let the server decide. connected="#{bean.live}" (or rendered) is re-evaluated on every Ajax request by a PreRenderViewEvent listener; when it turns false, the socket is closed in the oncomplete of that request. Tie the expression to a view-scoped property.

  • Close after a one-time result. Call faces.push.close(clientId) in onmessage once the single expected message arrived.

Manual connection
<h:commandButton value="Start import" onclick="faces.push.open('progress')">
    <f:ajax listener="#{importer.start}"/>
</h:commandButton>
<f:websocket id="progress" channel="importProgress" scope="view" connected="false"
             onmessage="showProgress"/>

Observed:

  • connected="false", then faces.push.open('live') and later faces.push.close('live') from the browser console: onopen and, after the close, onclose with code 1000 ran on Mojarra and MyFaces.

  • connected="#{liveToggle.live} with a view-scoped bean flipped by an Ajax button: on Mojarra the socket closed (onclose code 1000), pushes stopped, and flipping back reopened it. On MyFaces 4.1.4 the socket stayed open and kept receiving after live became false, with or without the tag in the render list. If the same code must run on MyFaces, drive the connection from JavaScript with faces.push.open/close.

Server Events: WebsocketEvent

Faces fires CDI events when a socket opens or closes: jakarta.faces.event.WebsocketEvent with the qualifiers @WebsocketEvent.Opened and @WebsocketEvent.Closed. The observer must be in an application-scoped bean (there is no request around those events). The event offers getChannel(), getUser() (generic, cast to your user type; a ClassCastException for the wrong type) and getCloseCode(), which is null for an opened event:

web/FeedObserver.java
import jakarta.faces.event.WebsocketEvent;
import jakarta.faces.event.WebsocketEvent.Closed;
import jakarta.faces.event.WebsocketEvent.Opened;

@ApplicationScoped
public class FeedObserver {

    private static final Logger log = Logger.getLogger(FeedObserver.class.getName());

    public void onOpen(@Observes @Opened WebsocketEvent event) {
        log.info("socket opened on " + event.getChannel() + " for " + event.getUser());
    }

    public void onClose(@Observes @Closed WebsocketEvent event) {
        log.info("socket closed on " + event.getChannel() + ", code " + event.getCloseCode());
    }
}

With an observer that printed the three values, both implementations reported channel=alerts user=null code=null when a client connected and code=NORMAL_CLOSURE when it closed the socket normally. A single person can have several sockets on one channel (tabs), so a "users online" counter must count distinct users, not open events. Use these events for presence and statistics, never as a security check.

Security

1. Protect the push URL as well as the page. The handshake URL is /jakarta.faces.push/<channel> (the constant PushContext.URI_PREFIX). A security-constraint on /secure/* protects the page that contains the tag, not the socket. Add the channel URL to the protected patterns:

web.xml
<security-constraint>
    <web-resource-collection>
        <web-resource-name>Members only</web-resource-name>
        <url-pattern>/secure/*</url-pattern>
        <url-pattern>/jakarta.faces.push/secureFeed</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>USER</role-name>
    </auth-constraint>
</security-constraint>

In the scratch run (a page under /secure/ with the channel secureFeed, BASIC authentication) a handshake with the session cookie but without credentials was refused on both implementations (the client saw close code 1006 and never an onopen), and with credentials it opened. When the url-pattern line for the channel was removed, the same cookie-only handshake opened successfully. So the extra pattern is what enforces the role.

2. The registered-channel check. The documentation adds that f:websocket records every channel it rendered in the current HTTP session and that an incoming handshake for an unknown channel id (guessed, spoofed, or reused after the session expired) is closed with 1008 (policy violated). Observed with a view-scoped channel: a different session’s cookie, or no cookie, could not connect to the owner’s channel (Mojarra refused the handshake, client code 1006; MyFaces opened and closed with 1011); a random id on an application-scoped channel was refused by Mojarra, and MyFaces closed it with 1011. Application-scoped channels have no owner: on Mojarra a handshake without any cookie succeeded for a valid application-scope id. Never send private data on an application-scoped channel.

3. Do not trust the browser. Use the authenticated identity for user (see above). The socket carries no commands, so there is nothing to authorise on the server beyond who may subscribe. Anything the browser wants to do goes through Faces Ajax, which keeps the view state, the session and your method-level security.

4. Escape on the client. Push messages often carry text from other users. Build DOM nodes with textContent; never assign pushed strings to innerHTML, and do not interpolate them into eval, Function or URL schemes.

5. Use TLS. Serve the page over HTTPS so the socket is encrypted (wss://), and keep the session cookie Secure and HttpOnly (see the security page). The scratch runs used plain HTTP.

6. Limit what is sent. The message is the JSON of the object’s getters. A DTO that holds only what every subscriber may see avoids leaking fields by accident.

7. Bound the cost. Every open socket is a server resource. MyFaces reads the parameters org.apache.myfaces.WEBSOCKET_MAX_IDLE_TIMEOUT (default 300000 ms, as logged at startup) and org.apache.myfaces.WEBSOCKET_MAX_CONNECTIONS, and org.apache.myfaces.RANDOM_KEY_IN_WEBSOCKET_SESSION_TOKEN (default secureRandom). Their meaning beyond the names and the logged default was not tested. On Mojarra the limits come from the server’s own WebSocket container settings.

Behind a Proxy and in a Cluster

Application- and user-targeted channels are kept in the memory of the JVM that rendered the page. Two consequences (derived from the design, not run in the scratch app):

  • A send on node A reaches only the sockets connected to node A. A cluster needs a way to broadcast the event to every node (a message queue or a cluster-wide CDI event) and have each node call PushContext.send locally.

  • The reverse proxy in front of the servers must forward WebSocket upgrade requests and keep idle connections open longer than the proxy’s default timeout. OmniFaces recommends enabling session persistence so a socket can reconnect after a restart or on another node; the Faces documentation only promises automatic reconnects at growing intervals.

The deployment and clustering page covers sticky sessions and session replication in prose.

OmniFaces o:socket

OmniFaces had the same feature before Faces did: o:socket (available since OmniFaces 2.3) is a UI component with the same attributes (channel, scope, user, onopen, onmessage, onerror, onclose, connected, rendered) and a very similar documentation. It is present in OmniFaces 5.5.5, the version used for the comparison on this page (the showcase page is current and no deprecation was found). The differences to know:

Topic f:websocket (Faces 4.1) o:socket (OmniFaces 5)

Sender API

jakarta.faces.push.Push and PushContext

org.omnifaces.cdi.Push and org.omnifaces.cdi.PushContext (same shape)

Enabling

jakarta.faces.ENABLE_WEBSOCKET_ENDPOINT=true

Automatic since OmniFaces 5.2 when @Inject @Push PushContext is used; otherwise org.omnifaces.SOCKET_ENDPOINT_ENABLED=true

Handshake path

/jakarta.faces.push/<channel>

/omnifaces.socket/<channel>

Open and close from JavaScript

faces.push.open(clientId), faces.push.close(clientId)

OmniFaces.Push.open(channel), OmniFaces.Push.close(channel)

Port

Context parameter jakarta.faces.WEBSOCKET_ENDPOINT_PORT

Attribute port on the tag

Server events

WebsocketEvent with Opened, Closed

SocketEvent with Opened, Switched (the user changed), Closed

Limits

Implementation specific

org.omnifaces.SOCKET_ENDPOINT_IDLE_TIMEOUT, org.omnifaces.SOCKET_MAX_SESSIONS_PER_CHANNEL

o:socket
<o:socket channel="reviews" onmessage="showReview"/>
Sender with the OmniFaces annotation
import org.omnifaces.cdi.Push;
import org.omnifaces.cdi.PushContext;

@Inject
@Push(channel = "reviews")
private PushContext reviews;

Choose f:websocket for portability and no extra dependency; choose o:socket if the application already uses OmniFaces and wants its extra events or limits. Do not mix the two annotations in one class: they are different types. The OmniFaces page covers installing OmniFaces and its push support; the same showcase also has o:sse (Server-Sent Events) for one-way text push over plain HTTP, which Faces itself does not provide (a proposal exists in the Faces issue tracker, #1587, without a milestone on 2026-10-08).

PrimeFaces: p:push Is Gone, Use f:websocket

PrimeFaces Push (the push feature of PrimeFaces 4 to 6) was removed in PrimeFaces 7.0; the 7.0 migration guide says to use the JSF 2.3 socket or OmniFaces instead. A PrimeFaces 16 application therefore pushes with f:websocket or o:socket, and updates components from the listener with a PrimeFaces p:remoteCommand (described on the PrimeFaces Ajax page) or the standard h:commandScript described above. When push is overkill, PrimeFaces still has p:poll, an Ajax component that sends periodic requests (<p:poll listener="#{counterBean.increment}" update="txt_count"/> inside a form). Polling costs a request per interval per client but needs no WebSocket support in the server and passes every proxy; it is a reasonable fallback for a dashboard that refreshes every 30 seconds.

Testing and Debugging a Push

  • Browser tools. In the Network tab filter by WS: it shows the handshake URL and each frame. A frame on Mojarra looks like {"data":{"reviewer":"Ana",...}}, on MyFaces like {"reviewer":"Ana",...}.

  • Is it a naming problem? If nothing arrives, compare the channel in the tag with the injection point’s field name (The Server Side: @Push and PushContext) and check the endpoint parameter (Enabling the Endpoint).

  • A scripted client. Node.js has a global WebSocket (22 and later); the headers option used below was run on Node.js 24. This script reads the page, extracts the socket URL from the faces.push.init call, connects with the page’s session cookie and prints the frames. It is how most of the observations on this page were made:

ws-client.mjs
const base = "http://localhost:8080/bookshelf";
const response = await fetch(`${base}/live.xhtml`);
const cookie = response.headers.getSetCookie().map(c => c.split(";")[0]).join("; ");
const html = await response.text();
const [, , path] = html.match(/faces\.push\.init\('([^']*)','([^']*)'/);
const url = path.startsWith("/") ? `ws://localhost:8080${path}` : path;   // MyFaces renders a relative URL

const socket = new WebSocket(url, { headers: { Cookie: cookie } });
socket.onopen = () => console.log("open");
socket.onmessage = event => console.log("message", event.data);
socket.onclose = event => console.log("close", event.code);

In an automated test, drive the page with Selenium or Playwright (see the end-to-end testing page of this repository, E2E testing in real browsers) and wait for the DOM change the push causes instead of sleeping. The Faces testing page is the place for Arquillian and unit-level strategies.

Mojarra versus MyFaces

Everything observed on Tomcat 11.0.26 and Weld 6.0.4 is collected here (the details are in the sections above):

Behaviour Mojarra 4.1.17 MyFaces 4.1.4

Wire format

{"data": ...} envelope

Bare JSON

Endpoint parameter missing

Clear error at render time

Renders, but the socket cannot connect

Invalid channel name, scope or channel reuse across scopes

Validated, descriptive errors

Mostly accepted

Listener defined after the tag

ReferenceError

Works

Records, enums, LocalDateTime as message

{} or an exception in send

Invalid JSON (record) or nested objects

PushContext of a view-scoped channel in an application-scoped bean

Bound to the first view or session

Worked for both users

Server-driven connected through Ajax

Closes and reopens the socket

No effect observed

f:ajax listener without parameter

Works

MethodNotFoundException

Unauthorised handshake

Refused (client sees 1006)

Opened then closed (1011)

Relative or absolute socket URL

Absolute ws://host:port/...

Relative /context/jakarta.faces.push/...

Jakarta JSON Processing missing from the WAR (Tomcat 11.0.26)

NoClassDefFoundError: jakarta/json/Json on the first send; frame {"data":...}

Pushed without it; frame is a bare JSON string

What Changed by Version

  • JSF 2.3 (Java EE 8, 2017): f:websocket, @Push, PushContext, WebsocketEvent, UIWebsocket and the jsf.push JavaScript functions are introduced ("WebSocket Integration", issue 1396 in the specification’s changelog), together with h:commandScript. The context parameters are javax.faces.ENABLE_WEBSOCKET_ENDPOINT and javax.faces.WEBSOCKET_ENDPOINT_PORT.

  • Faces 3.0 (Jakarta EE 9): package and parameter prefix change from javax.faces to jakarta.faces; the tag namespace stays the legacy one. Jakarta WebSocket 2.0 replaces javax.websocket.

  • Faces 4.0 (Jakarta EE 10): the onerror attribute is added (issue 1573, milestone 4.0 in the Faces tracker); jsf.push becomes faces.push; the tag namespace is jakarta.faces.core.

  • Faces 4.1 (Jakarta EE 11): the 4.1 change log lists nothing about f:websocket, @Push or PushContext; the optional dependency is Jakarta WebSocket 2.2.

  • Faces 5.0 (upcoming, Jakarta EE 12; latest milestone 5.0.0-M9 on 2026-09-18): a search of the Faces issue tracker on 2026-10-08 found no 5.0 change specific to f:websocket. Open issues on 2026-10-08: a JavaScript API to register WebSocket event listeners (#1442, milestone 5.1) and a Server-Sent Events alternative (#1587, no milestone). Do not rely on either.

The Jakarta EE Tutorial page for WebSockets is marked "last updated for Jakarta EE Version 9.1": it does not mention onerror, which the 4.0 VDL lists.

Where the Books Are Outdated

  • Ed Burns and Chris Schalk’s JavaServer Faces 2.0: The Complete Reference (2010) and the other JSF 2.0 and earlier books predate JSF 2.3: they have no server push in the framework. Their answers to "live" pages are polling with Ajax, hand-written XMLHttpRequest code and third-party libraries. The current answer is f:websocket for push and f:ajax or h:commandScript for the request back.

  • Pro JSF and Ajax (2006) and JSF 2.0 Cookbook (2010) describe Ajax4jsf, Dynamic Faces, Weblets and RichFaces push-style components; all of those are gone (see the Ajax page for the standard replacement).

  • PrimeFaces Cookbook, Second Edition (2015) and Learning PrimeFaces Extensions Development (2014) were written for PrimeFaces 4 and 5, where push meant PrimeFaces Push. It was removed in PrimeFaces 7.0; use f:websocket or o:socket.

  • Everything those books show uses the javax.faces packages and, where a WebSocket server is mentioned at all, the non-standard APIs of the day. The current packages are jakarta.faces.push, jakarta.faces.component.UIWebsocket and jakarta.faces.event.WebsocketEvent.

Pitfalls

  • Forgot the context parameter. Mojarra tells you; MyFaces renders a socket that never opens.

  • The channel name is the field name. @Inject @Push PushContext reviewsChannel sends to reviewsChannel.

  • EL in channel or scope. Not allowed. Use fixed names; one global channel with a typed message is better than many per-view channels.

  • Listeners declared after the tag. A ReferenceError on Mojarra.

  • Sending records, enums or java.time values. Silent {}, invalid JSON, or an exception, depending on the implementation. Send strings, numbers and simple beans.

  • Session or view scope from an application-scoped bean. Wrong audience on Mojarra. Send from a bean in the same request, or use user.

  • user from a request parameter. Anyone can subscribe to anyone else’s messages.

  • Private data on an application channel. Everyone can connect.

  • Unprotected push URL. Protect /jakarta.faces.push/<channel> as well as the page.

  • innerHTML with pushed text. Cross-site scripting.

  • Sending in the same instant as onopen. Registration finishes slightly later; load the current state in the page.

  • Treating push as delivery. Missed messages are not replayed.

  • Tomcat without JSON-P. NoClassDefFoundError: jakarta/json/Json on the first send with Mojarra.