How to Build Server-Sent Events with JAX-RS
Server-Sent Events (SSE) let a JAX-RS application keep an HTTP connection open and send updates to a client as they become available. JAX-RS 2.1, included in Java EE 8, provides a standard SSE API for creating events, managing connections, broadcasting messages, and consuming an event stream.
This tutorial builds a JAX-RS SSE service that starts a background task, returns the URI of an event stream, registers multiple clients, and broadcasts a message to every registered client. It also explains how the server and client APIs fit together and what should be changed before using this design in production.
How JAX-RS Server-Sent Events Work
SSE uses a normal, long-lived HTTP response with the media type text/event-stream. Communication is one-way: the server pushes events over the stream, while the client uses separate HTTP requests when it needs to send data back.
Ssecreates events, broadcasters, and related SSE objects.SseEventSinkrepresents one open server-to-client connection.SseBroadcastersends an event to multiple registered sinks.SseEventSourceis the JAX-RS client API for opening and reading an SSE stream.OutboundSseEventrepresents an event created by the server.InboundSseEventrepresents an event received by a JAX-RS client.
SSE is a good fit for notifications, progress updates, monitoring data, and other streams in which most communication travels from the server to the client. Use WebSockets when the application requires continuous, low-latency communication in both directions.
Add the Java EE 8 API Dependency
The example uses the Java EE 8 form of the JAX-RS API, whose packages begin with javax.ws.rs. Add the following provided dependency when deploying to a Java EE 8 application server:
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>8.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
The provided scope is appropriate because the target application server supplies the Java EE APIs at runtime. In a Jakarta EE application, use a Jakarta EE API version supported by the server and replace javax.* imports with the corresponding jakarta.* imports. Do not mix the two namespaces in one application.
Activate the JAX-RS Application
Create an Application subclass to define the base path for the REST resources:
@ApplicationPath("webresources")
public class ApplicationConfig extends Application {
}
With an application context such as /ch03-sse, resources are available below /ch03-sse/webresources. The complete URL also depends on the host, port, and deployment name configured by the server.
Create the JAX-RS SSE Resource and Broadcaster
The resource below exposes two operations. A POST request to start creates a background process and returns the URI that clients should open. A GET request to register/{id} establishes the SSE connection and registers its event sink with the process broadcaster.
@Path("serverSentService")
@RequestScoped
public class ServerSentService {
private static final Map<Long, UserEvent> POOL =
new ConcurrentHashMap<>();
@Resource(name = "LocalManagedExecutorService")
private ManagedExecutorService executor;
@Path("start")
@POST
public Response start(@Context Sse sse) {
final UserEvent process = new UserEvent(sse);
POOL.put(process.getId(), process);
executor.submit(process);
final URI uri = UriBuilder.fromResource(ServerSentService.class).path
("register/{id}").build(process.getId());
return Response.created(uri).build();
}
@Path("register/{id}")
@Produces(MediaType.SERVER_SENT_EVENTS)
@GET
public void register(@PathParam("id") Long id,
@Context SseEventSink sseEventSink) {
final UserEvent process = POOL.get(id);
if (process != null) {
process.getSseBroadcaster().register(sseEventSink);
} else {
throw new NotFoundException();
}
}
static class UserEvent implements Runnable {
private final Long id;
private final SseBroadcaster sseBroadcaster;
private final Sse sse;
UserEvent(Sse sse) {
this.sse = sse;
this.sseBroadcaster = sse.newBroadcaster();
id = System.currentTimeMillis();
}
Long getId() {
return id;
}
SseBroadcaster getSseBroadcaster() {
return sseBroadcaster;
}
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);
sseBroadcaster.broadcast(sse.newEventBuilder().
name("register").data(String.class, "Text from event "
+ id).build());
sseBroadcaster.close();
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
}
}
Start the Background SSE Process
The start() method creates a UserEvent, stores it in a concurrent map, and submits it to a container-managed executor:
@Path("start")
@POST
public Response start(@Context Sse sse) {
final UserEvent process = new UserEvent(sse);
POOL.put(process.getId(), process);
executor.submit(process);
final URI uri = UriBuilder.fromResource(ServerSentService.class).
path("register/{id}").build(process.getId());
return Response.created(uri).build();
}
The response uses HTTP status 201 Created. Its Location header identifies the stream endpoint for this process. A client first calls start, reads that header, and then opens the returned URI.
The resource obtains the JAX-RS SSE factory from the request context:
@Context Sse sse
@Context is JAX-RS context injection. The injected Sse object is used to create the broadcaster and outbound event builder.
Register Each Client Event Sink
The registration method declares the SSE media type and receives an SseEventSink representing the connected client:
@Path("register/{id}")
@Produces(MediaType.SERVER_SENT_EVENTS)
@GET
public void register(@PathParam("id") Long id,
@Context SseEventSink sseEventSink) {
final UserEvent event = POOL.get(id);
if (event != null) {
event.getSseBroadcaster().register(sseEventSink);
} else {
throw new NotFoundException();
}
}
MediaType.SERVER_SENT_EVENTS corresponds to text/event-stream. If the process exists, the method attaches the client’s sink to its broadcaster. If the identifier is unknown, the resource returns a 404 response through NotFoundException.
@Context SseEventSink sseEventSink
...
event.getSseBroadcaster().register(sseEventSink);
An SseEventSink is an abstraction over an open SSE connection, not an application-managed event queue. Registering it with the broadcaster causes later broadcasts to be written to that connection. The broadcaster can manage several sinks for the same process.
Build and Broadcast the Outbound SSE Message
The nested UserEvent task creates its broadcaster in the constructor. After a five-second delay, it creates a named event containing string data, broadcasts it, and closes the broadcaster:
static class UserEvent implements Runnable {
...
UserEvent(Sse sse) {
this.sse = sse;
this.sseBroadcaster = sse.newBroadcaster();
id = System.currentTimeMillis();
}
...
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);
sseBroadcaster.broadcast(sse.newEventBuilder().
name("register").data(String.class, "Text from event "
+ id).build());
sseBroadcaster.close();
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
}
The broadcaster is created by the injected Sse factory:
this.sseBroadcaster = sse.newBroadcaster();
The task sends an event named register. The data payload is a Java String that the JAX-RS runtime serializes into the event stream:
sseBroadcaster.broadcast(sse.newEventBuilder().name("register").
data(String.class, "Text from event " + id).build());
Closing the broadcaster ends this example’s stream after one message. For a continuous feed, keep the broadcaster open while the process is active and close it during explicit cleanup or application shutdown.
Consume the JAX-RS SSE Stream from Java
The following JSF backing bean acts as a test client. It calls the start endpoint, creates the requested number of SseEventSource instances, receives the broadcast asynchronously, and displays the results as JSF messages.
@ViewScoped
@Named
public class SseBean implements Serializable {
@NotNull
@Positive
private Integer countClient;
private Client client;
@PostConstruct
public void init(){
client = ClientBuilder.newClient();
}
@PreDestroy
public void destroy(){
client.close();
}
public void sendEvent() throws URISyntaxException, InterruptedException {
WebTarget target = client.target(URI.create("http://localhost:8080/
ch03-sse/"));
Response response =
target.path("webresources/serverSentService/start")
.request()
.post(Entity.json(""), Response.class);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage("Sse Endpoint: " +
response.getLocation()));
final Map<Integer, String> messageMap = new ConcurrentHashMap<>
(countClient);
final SseEventSource[] sources = new
SseEventSource[countClient];
final String processUriString =
target.getUri().relativize(response.getLocation()).
toString();
final WebTarget sseTarget = target.path(processUriString);
for (int i = 0; i < countClient; i++) {
final int id = i;
sources[id] = SseEventSource.target(sseTarget).build();
sources[id].register((event) -> {
final String message = event.readData(String.class);
if (message.contains("Text")) {
messageMap.put(id, message);
}
});
sources[i].open();
}
TimeUnit.SECONDS.sleep(10);
for (SseEventSource source : sources) {
source.close();
}
for (int i = 0; i < countClient; i++) {
final String message = messageMap.get(i);
FacesContext.getCurrentInstance().addMessage(null,
new FacesMessage("Message sent to client " +
(i + 1) + ": " + message));
}
}
public Integer getCountClient() {
return countClient;
}
public void setCountClient(Integer countClient) {
this.countClient = countClient;
}
}
The first request starts the server task and retrieves the stream URI from the response:
WebTarget target = client.target(URI.create
("http://localhost:8080/ch03-sse/"));
Response response = target.path("webresources/serverSentService
/start")
.request()
.post(Entity.json(""), Response.class);
Each SseEventSource then opens the same stream URI. Its registered callback runs whenever an event arrives:
for (int i = 0; i < countClient; i++) {
final int id = i;
sources[id] = SseEventSource.target(sseTarget).build();
sources[id].register((event) -> {
final String message = event.readData(String.class);
if (message.contains("Text")) {
messageMap.put(id, message);
}
});
sources[i].open();
}
The callback reads the event data as a String and stores it by client identifier. After the sources have been closed, the bean obtains each stored value:
final String message = messageMap.get(i);
A production client should also register handlers for connection errors and completion. It should close both the event source and the initial Response when they are no longer needed.
Add the JSF Controls for the SSE Test Clients
The JSF page accepts the number of simulated clients and invokes the backing bean:
<h:body>
<h:form>
<h:outputLabel for="countClient" value="Number of Clients" />
<h:inputText id="countClient" value="#{sseBean.countClient}" />
<br />
<h:commandButton type="submit" action="#{sseBean.sendEvent()}"
value="Send Events" />
</h:form>
</h:body>
The relevant value binding and action are:
<h:inputText id="countClient" value="#{sseBean.countClient}" />
...
<h:commandButton type="submit" action="#{sseBean.sendEvent()}" value="Send Events" />
Submitting the form runs a synchronous JSF action that deliberately waits while the asynchronous clients receive their messages. That makes the flow easy to demonstrate, but a real web application should avoid sleeping inside a request thread. Update the view asynchronously or let the browser connect directly to the SSE endpoint instead.
Connect a Browser with the EventSource API
Browsers provide the EventSource API for SSE. Because this example first creates a process with POST, the browser must read the returned Location header before opening the stream. The following example assumes that the page and resource are served from the same origin:
async function startSse() {
const response = await fetch('/ch03-sse/webresources/serverSentService/start', {
method: 'POST'
});
if (!response.ok) {
throw new Error(`Unable to start SSE process: ${response.status}`);
}
const streamUrl = response.headers.get('Location');
if (!streamUrl) {
throw new Error('The response did not contain a Location header');
}
const source = new EventSource(streamUrl);
source.addEventListener('register', event => {
console.log('SSE message:', event.data);
source.close();
});
source.onerror = error => {
console.error('SSE connection error', error);
source.close();
};
}
startSse().catch(console.error);
The event listener name must match the name assigned by name("register") on the server. For unnamed events, use the browser’s onmessage handler instead.
Test the JAX-RS Event Stream
Deploy the application, call the start endpoint, and inspect the response headers. The returned Location value contains the process-specific registration URL. Open that URL with an SSE-capable client before the five-second task delay expires.
You can also inspect a known stream URL with curl. The -N option disables output buffering so that events appear as they arrive:
curl -N -H 'Accept: text/event-stream' \
http://localhost:8080/ch03-sse/webresources/serverSentService/register/PROCESS_ID
Replace PROCESS_ID with the identifier present in the Location header. A successful stream emits fields similar to the following before the example closes the connection:
event: register
data: Text from event 1530000000000
JAX-RS SSE Lifecycle and Production Considerations
- Remove completed processes: the example leaves each
UserEventin the staticPOOL. Remove entries when work finishes or expires to prevent unbounded memory use. - Use collision-resistant identifiers:
System.currentTimeMillis()can produce duplicate values when requests start in the same millisecond. Use a UUID or another identifier strategy for production work. - Handle disconnects: clients may close a tab, lose connectivity, or time out. Close failed sinks and unregister them from application-managed state.
- Send heartbeats: comments or lightweight events can help keep an otherwise idle stream active through intermediaries, subject to the application’s network environment.
- Review proxy buffering and timeouts: reverse proxies, gateways, and load balancers must permit long-lived responses and should not buffer the event stream.
- Secure both endpoints: authenticate and authorize the start request and stream request. Do not assume that knowing a process identifier grants access.
- Validate cross-origin requirements: when the browser and API use different origins, configure CORS deliberately and confirm how credentials and response headers are exposed.
- Avoid blocking request threads: do not copy the demonstration’s ten-second sleep into request-processing code. Use asynchronous UI behavior and container-managed background execution.
- Plan for clustered deployments: a static in-memory map belongs to one application instance. Distributed deployments require routing affinity or shared messaging and state.
SSE connections consume HTTP connections for as long as they remain open. Capacity testing should include the expected number of concurrent clients, event frequency, payload size, reconnect behavior, proxy configuration, and application-server limits.
JAX-RS Server-Sent Events FAQ
What is the difference between SseEventSink and SseBroadcaster?
An SseEventSink represents one open connection to one client. An SseBroadcaster manages multiple registered sinks and sends an outbound event to all of them.
Can a JAX-RS SSE client send data back over the same connection?
No. An SSE stream carries events from the server to the client. The client can send data back through a separate HTTP request, such as POST, or the application can use WebSockets when it needs full-duplex communication.
How does a Java client consume Server-Sent Events?
JAX-RS 2.1 clients can build an SseEventSource for a WebTarget, register callbacks for incoming events and errors, and call open(). The source should be closed when the stream is no longer required.
Should JAX-RS SSE streams use GET or POST?
The stream itself is normally exposed through GET with the text/event-stream response type. This tutorial uses a separate POST to create a process and then returns the URI of its GET stream.
When should WebSockets be used instead of SSE?
Use WebSockets when the server and client must exchange frequent messages in both directions over one persistent connection. SSE is simpler when the persistent channel is primarily for server-to-client updates and ordinary HTTP requests can handle client actions.
JAX-RS SSE Tutorial QA Checklist
- Confirm that the application server supports the JAX-RS 2.1 SSE API used by Java EE 8.
- Verify that the project consistently uses either the
javaxorjakartanamespace required by its runtime. - Check that the registration endpoint produces
MediaType.SERVER_SENT_EVENTS. - Confirm that the client opens the URI returned in the start response’s
Locationheader. - Verify that event sources, sinks, broadcasters, HTTP responses, and completed process entries are closed or removed.
- Test stream behavior through the actual reverse proxy or load balancer, including idle timeouts and buffering.
- Test authentication and authorization for both process creation and event-stream registration.
- Run concurrent-client tests using realistic connection counts and event rates.
TutorialKart.com