Three Spring Async Streaming APIs to Eliminate Timeout Issues

This article explains how to handle long‑running Spring endpoints by using three asynchronous streaming tools—ResponseBodyEmitter, SseEmitter, and StreamingResponseBody—showing their appropriate scenarios, configuration details, and complete code examples that keep the servlet thread free and avoid timeout problems.

Java Companion
Java Companion
Java Companion
Three Spring Async Streaming APIs to Eliminate Timeout Issues

When an endpoint performs a time‑consuming operation, traditional asynchronous approaches such as Callable, WebAsyncTask, DeferredResult or CompletableFuture can offload the work but only return a single result, which is insufficient for scenarios that require continuous feedback to the client.

ResponseBodyEmitter

ResponseBodyEmitter

is suitable for dynamically generating content and sending incremental updates, such as file‑upload progress or real‑time logs. The article demonstrates creating an emitter, running a simulated long‑running task, and calling emitter.send(...) repeatedly every two seconds, finally invoking emitter.complete(). The emitter’s timeout can be disabled by setting it to 0 or -1.

@GetMapping("/bodyEmitter")
public ResponseBodyEmitter handle() {
    // -1 means no timeout
    ResponseBodyEmitter emitter = new ResponseBodyEmitter(-1L);
    CompletableFuture.runAsync(() -> {
        try {
            for (int i = 0; i < 10000; i++) {
                System.out.println("bodyEmitter " + i);
                emitter.send("bodyEmitter " + i + " @ " + new Date() + "
");
                Thread.sleep(2000);
            }
            emitter.complete();
        } catch (Exception e) {
            emitter.completeWithError(e);
        }
    });
    return emitter;
}

The demo shows a simple text stream that updates the page every two seconds, mimicking the incremental response behavior of GPT‑style answers.

SseEmitter

SseEmitter

extends ResponseBodyEmitter and is designed for server‑to‑client push of real‑time data, such as live notifications or status updates. It uses the text/event-stream MIME type, establishing a one‑way channel that can survive server restarts via automatic reconnection.

On the client side, a single EventSource request receives messages and appends them to the DOM. The server stores each SseEmitter in a concurrent map keyed by user ID, allowing other endpoints to retrieve the emitter and push data with send.

private static final Map<String, SseEmitter> EMITTER_MAP = new ConcurrentHashMap<>();

@GetMapping("/subSseEmitter/{userId}")
public SseEmitter sseEmitter(@PathVariable String userId) {
    log.info("sseEmitter: {}", userId);
    SseEmitter emitterTmp = new SseEmitter(-1L);
    EMITTER_MAP.put(userId, emitterTmp);
    CompletableFuture.runAsync(() -> {
        try {
            SseEmitter.SseEventBuilder event = SseEmitter.event()
                .data("sseEmitter" + userId + " @ " + LocalTime.now())
                .id(String.valueOf(userId))
                .name("sseEmitter");
            emitterTmp.send(event);
        } catch (Exception ex) {
            emitterTmp.completeWithError(ex);
        }
    });
    return emitterTmp;
}

@GetMapping("/sendSseMsg/{userId}")
public void sseEmitter(@PathVariable String userId, String msg) throws IOException {
    SseEmitter sseEmitter = EMITTER_MAP.get(userId);
    if (sseEmitter == null) return;
    sseEmitter.send(msg);
}

Sending a request to 127.0.0.1:9033/sendSseMsg/7777?msg=Welcome displays the message instantly on the client page, and the connection automatically reconnects if the server restarts.

StreamingResponseBody

StreamingResponseBody

differs from the previous two by focusing on large data volumes or continuous streams, writing directly to the response OutputStream. It is ideal for downloading huge files without loading the entire content into memory.

@GetMapping("/streamingResponse")
public ResponseEntity<StreamingResponseBody> handleRbe() {
    StreamingResponseBody stream = out -> {
        String message = "streamingResponse";
        for (int i = 0; i < 1000; i++) {
            try {
                out.write((message + i + "
").getBytes());
                out.write("
".getBytes());
                out.flush();
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    return ResponseEntity.ok()
        .contentType(MediaType.TEXT_HTML)
        .body(stream);
}

The demo streams simple text lines; replacing the string with a file input stream yields the same effect for large file downloads.

Conclusion

The three Spring tools— ResponseBodyEmitter, SseEmitter, and StreamingResponseBody —provide straightforward ways to implement asynchronous streaming interfaces, each fitting different use cases such as incremental updates, server‑push events, and large‑file streaming. Using them can improve system responsiveness and avoid timeout problems.

Demo GitHub repository: https://github.com/chengxy-nds/Springboot-Notebook/tree/master/springboot101/通用功能/springboot-streaming
StreamingSpringAsyncResponseBodyEmitterStreamingResponseBodySseEmitter
Java Companion
Written by

Java Companion

A highly professional Java public account

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.