Spring Boot cookbook · Intermediate

Stream progress updates with Server-Sent Events

Show live progress of a long-running import in the browser.

Approach

Return an SseEmitter from a controller, register it per job, and send events from the worker as progress changes. Set a timeout, clean up on completion/timeout/error, and send heartbeats so proxies don't close idle connections.

Solution

@RestController
@RequestMapping("/api/v1/imports")
class ImportProgressController {
    private final Map<String, List<SseEmitter>> emitters = new ConcurrentHashMap<>();

    @GetMapping(path = "/{jobId}/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    SseEmitter subscribe(@PathVariable String jobId) {
        SseEmitter emitter = new SseEmitter(Duration.ofMinutes(10).toMillis());
        emitters.computeIfAbsent(jobId, k -> new CopyOnWriteArrayList<>()).add(emitter);
        Runnable remove = () -> emitters.getOrDefault(jobId, List.of()).remove(emitter);
        emitter.onCompletion(remove);
        emitter.onTimeout(remove);
        emitter.onError(e -> remove.run());
        return emitter;
    }

    // Called by the import worker
    void publish(String jobId, int percent) {
        for (SseEmitter emitter : emitters.getOrDefault(jobId, List.of())) {
            try {
                emitter.send(SseEmitter.event().name("progress").data(Map.of("percent", percent)));
                if (percent >= 100) emitter.complete();
            } catch (IOException e) {
                emitter.completeWithError(e);
            }
        }
    }
}

Pitfalls

  • In-memory emitter registry only works on one instance — fan out via Redis pub/sub for multiple replicas.
  • Proxy buffering (disable for the SSE path).
  • Holding platform threads per connection without virtual threads or async support.

Variations

  • WebFlux Flux<ServerSentEvent<?>> for reactive stacks.
  • Polling a status endpoint for simplicity.

Further reading: Spring MVC: SSE

Reference: https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-async.html#mvc-ann-async-sse

More Spring Boot recipes