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
- Add correlation IDs to Spring Boot logs
- Add retries and a circuit breaker with Resilience4j
- Authenticate machine clients with API keys in Spring Security
- Cache service results in Redis with @Cacheable
- Consume Kafka messages with retries and a dead-letter topic
- Handle concurrent updates with optimistic locking
- Implement soft delete with Hibernate
- Make POST endpoints idempotent in Spring Boot