Three Core Ways to Implement Asynchronous Streaming Responses in Spring Boot

This article explains three Spring Boot 3.5.0 techniques—ResponseBodyEmitter, SseEmitter, and StreamingResponseBody—for delivering multiple asynchronous values as a stream, complete with code samples, curl demonstrations, and a front‑end EventSource example.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Three Core Ways to Implement Asynchronous Streaming Responses in Spring Boot

Environment: Spring Boot 3.5.0.

1. Introduction

In a Spring application a single asynchronous return value can be handled with DeferredResult or Callable. The following example shows a minimal endpoint that returns a DeferredResult<String> which will be completed by another thread.

@GetMapping("/message")
@ResponseBody
public DeferredResult<String> asyncMessage() {
    DeferredResult<String> deferredResult = new DeferredResult<>();
    return deferredResult;
}
// other thread sets the final result
deferredResult.setResult(result);

When a use case requires sending multiple asynchronous values—such as real‑time push— DeferredResult and Callable are insufficient. Spring provides three core implementations that can write multiple values to the HTTP response.

2. Practical Implementations

2.1 ResponseBodyEmitter

ResponseBodyEmitter

streams objects that are serialized by an HttpMessageConverter. It can be returned directly or wrapped in ResponseEntity.

@RestController
@RequestMapping("/streams")
public class AsyncStreamController {
    private final Map<Object, ResponseBodyEmitter> bodyEmitter = new ConcurrentHashMap<>();

    @GetMapping("/bodyemitter/{id}")
    public ResponseBodyEmitter body(@PathVariable String id) throws Exception {
        ResponseBodyEmitter emitter = this.bodyEmitter.computeIfAbsent(id, key -> {
            ResponseBodyEmitter rbe = new ResponseBodyEmitter();
            rbe.onCompletion(() -> this.bodyEmitter.remove(id));
            return rbe;
        });
        return emitter;
    }

    @GetMapping("/send/{id}")
    public void send(@PathVariable String id) throws Exception {
        ResponseBodyEmitter emitter = this.bodyEmitter.get(id);
        if (emitter != null) {
            emitter.send(
                Map.of("code", 0, "data", "T - %s".formatted(System.currentTimeMillis())),
                MediaType.APPLICATION_JSON);
        }
    }

    @GetMapping("/complete/{id}")
    public void complete(@PathVariable String id) throws Exception {
        ResponseBodyEmitter emitter = this.bodyEmitter.get(id);
        if (emitter != null) {
            emitter.complete();
        }
    }
}

Command‑line curl can be used to receive the streamed data. The screenshots below illustrate the interaction:

ResponseBodyEmitter demo
ResponseBodyEmitter demo
Send data via /send endpoint
Send data via /send endpoint
Complete stream via /complete endpoint
Complete stream via /complete endpoint

2.2 SseEmitter

SseEmitter

(a subclass of ResponseBodyEmitter) implements the Server‑Sent Events (SSE) specification. It can be used to push events that conform to the W3C SSE format.

private final Map<Object, SseEmitter> sseEmitter = new ConcurrentHashMap<>();

@GetMapping(path="/sse/{id}", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle(@PathVariable String id) {
    SseEmitter emitter = this.sseEmitter.computeIfAbsent(id, key -> {
        SseEmitter sse = new SseEmitter();
        sse.onCompletion(() -> this.sseEmitter.remove(id));
        sse.onError(System.err::println);
        return sse;
    });
    return emitter;
}

@GetMapping("/msg/{id}")
public void msg(@PathVariable String id) throws Exception {
    SseEmitter sse = this.sseEmitter.get(id);
    if (sse != null) {
        sse.send(
            Map.of("code", 0, "data", "T - %s".formatted(System.currentTimeMillis())),
            MediaType.APPLICATION_JSON);
    }
}

The front‑end page uses the native EventSource API to consume the SSE stream. The essential HTML/JavaScript is shown below.

<body>
  <div class="container">
    <h1>Server‑Sent Events (SSE) Demo</h1>
    <div class="button-container">
      <div class="input-group">
        <input type="text" id="username" placeholder="Enter username" autocomplete="off">
      </div>
      <button class="btn-primary" type="button" onclick="connect()" id="connectBtn">Connect</button>
      <button class="btn-danger" type="button" onclick="closeSse()" id="disconnectBtn" disabled>Close Connection</button>
    </div>
    <div class="status" id="status">Waiting for connection...</div>
    <hr/>
    <ul id="list"></ul>
  </div>
  <script>
    const statusElement = document.querySelector("#status");
    const eventList = document.querySelector("#list");
    const disconnectBtn = document.querySelector("#disconnectBtn");
    const userName = document.querySelector("#username");
    let eventSource;

    function connect() {
      eventSource = new EventSource(`/streams/sse/${userName.value}`);
      eventSource.onmessage = event => {
        const li = document.createElement("li");
        li.innerHTML = "<strong>Received message:</strong> " + event.data;
        eventList.appendChild(li);
      };
      eventSource.onopen = () => {
        statusElement.textContent = "Connection established ✓";
        statusElement.style.color = "#27ae60";
        disconnectBtn.disabled = false;
        console.log('Connection opened');
      };
      eventSource.onerror = () => {
        statusElement.textContent = "Connection error ✗";
        statusElement.style.color = "#e74c3c";
        disconnectBtn.disabled = true;
        console.error('Error occurred');
      };
    }

    function closeSse() {
      eventSource.close();
      statusElement.textContent = "Connection closed";
      statusElement.style.color = "#e74c3c";
      disconnectBtn.disabled = true;
      console.log('Connection closed');
    }
  </script>
</body>

2.3 StreamingResponseBody

When the goal is to bypass message conversion and write raw bytes directly to the response output stream—common in file‑download scenarios— StreamingResponseBody can be used.

@GetMapping("/streambody")
public ResponseEntity<StreamingResponseBody> streamUsers() {
    List<User> datas = List.of(
        new User("张三", 22),
        new User("李四", 23),
        new User("王五", 24),
        new User("Pack_xg", 33),
        new User("刘飞", 44));
    StreamingResponseBody responseBody = os -> {
        datas.forEach(user -> {
            try {
                String json = this.objectMapper.writeValueAsString(user) + "
";
                os.write(json.getBytes());
                os.flush();
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
    };
    return ResponseEntity.ok()
        .header("Content-Type", "text/plain;charset=utf-8")
        .body(responseBody);
}

public static record User(String name, Integer age) {}

Invoking the endpoint with curl prints each JSON line sequentially, as shown in the screenshot. The same stream can also be consumed via an EventSource on the client side.

Curl output of StreamingResponseBody
Curl output of StreamingResponseBody

The article demonstrates that Spring Boot offers flexible mechanisms— ResponseBodyEmitter, SseEmitter, and StreamingResponseBody —to implement asynchronous streaming responses suitable for real‑time messaging, server‑sent events, and raw byte streaming.

JavaStreamingAsynchronousSpring BootResponseBodyEmitterStreamingResponseBodySseEmitter
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.