To make an AI chat answer appear word by word, have a Spring MVC controller return an SseEmitter with the content type text/event-stream. A worker thread reads the model’s streamed output and forwards a small set of application events (text deltas, completion, and errors) to the browser, which renders each delta as it arrives. The server side is straightforward. The parts that cause real problems are the browser request method, disconnect detection, proxy buffering, and thread capacity, and this guide covers each of them.
What the browser actually receives
Server-Sent Events (SSE) is a one-way text protocol over a normal HTTP response. The server keeps the response open and writes messages; the browser reads them as they arrive. The WHATWG HTML Living Standard defines the format, and the section describing it was last updated on 6 October 2026 at the time of writing.
As an Amazon Associate I earn from qualifying purchases.
- The stream is UTF-8 text.
- Each message is a set of lines such as
event:,id:,data:, andretry:, and a blank line ends the message. - A line beginning with a colon is a comment. Comments are ignored by the browser but keep the connection active, which matters later.
- Several
data:lines in one message are joined with newline characters on the client.
event: delta
id: 42
data: {"text":"Hello"}
: keep-alive
event: done
data: {}
Sending JSON in data is a deliberate choice. Raw model text can contain newlines, and JSON keeps each message on one line so that framing never depends on how newlines are handled.
Choose the Spring programming model
Spring Framework’s reference documentation describes SseEmitter as “a subclass of ResponseBodyEmitter [that] provides support for Server-Sent Events, where events sent from the server are formatted according to the W3C SSE specification.” Spring also supports returning a reactive Flux<ServerSentEvent<?>> from WebFlux. Both work. The right one depends on how your application already calls the model.
#1 Best Overall
- [Natural Audio Clarity] Operated with frequency response of 50Hz-16KHz, the podcasting XLR mic delivers balanced audio range, likely to resonate with your audience. Directional cardioid dynamic microphone corded will not exaggerate your voice, while rejects unwanted off-axis noise for vocal originality and intelligibility during your PS5 gaming streaming video recording. (Tips: Keep the top of end-addressing XLR dynamic microphone AM8 facing audio source, and suggested recording range is 2 to 6 in.)
- [XLR Connection Upgrade-Ability] To use XLR connection, connect the podcast microphone to an audio interface (or mixer) using a separate XLR cable (NOT Included) . Well-connected and smooth operation improves audio flexibility to make you explore various types of music recording singing. The streaming mic isolates the pristine and accurate sound from ambient noise with greater no interference and fidelity. (RGB and function key on mic are INACTIVE when using XLR connection.)
- [USB Connection with Handy Mute] Skip the hassle of setting something up and plug the cable to play the dynamic USB microphone directly, which suits for beginner creators or daily podcast. You can quickly control the gamer mic with tap-to-mute that is independent of computer/Macbook programs to keep privacy when live streaming. LED mute reminder helps you get rid of forgetting to cancel the mute. (RGB and function key are only available for USB connection, but NOT for XLR connection)
- [Soothing Controllable RGB] RGB ring on the desktop gaming microphone for PC, with 3 modes and more than 10 light colors collection, matches your PC gears accessories for gaming synergy even in dim room. You can control the RGB key button of the dynamic microphone USB directly for game color scheme gaming or live streaming. Configured memory function, the streaming microphone RGB no need to repeated selections after turnning off and brings itself alive when power on. (Only available for USB connection)
- [More Function Keys] Computer microphone with headphones jack upgrades your rhythm game experience and gets feedback whether the real-time voice your audience hear as expected. Get the desired level via monitoring volume control when gaming recording. Smooth mic gain knob on the PC microphone gaming has some resistance to the point, easily for audio attenuation or boost presence to less post-production audio. (Only available for USB connection)
| Concern | Spring MVC with SseEmitter |
WebFlux with Flux<ServerSentEvent<?>> |
|---|---|---|
| Programming model | Imperative: your code calls send(), complete(), or completeWithError() |
Reactive: the stream is a publisher pipeline mapped to events |
| Fit for upstream client | Good when the model SDK is blocking or callback-based and you already relay values in a loop | Good when the model client returns a reactive publisher |
| Thread behavior | Response writes are blocking and run on an async worker thread; plan executor capacity (see below) | Asynchronous by design |
| Cancellation hooks | onCompletion, onTimeout, and onError callbacks |
Cancelling the subscription propagates upstream |
| Back-pressure | You control pacing in your own loop | Handled by the reactive pipeline |
If your application is a conventional Spring MVC app, SseEmitter is the lower-friction choice and is the approach used in the examples below. WebSocket is an alternative when the browser must send messages mid-stream. Chat completions usually don’t need that, because the prompt goes up once and the answer streams back down.
Define your own event contract
Model providers emit many event types. OpenAI’s Responses API streaming guide documents response.output_text.delta for incremental text and response.completed for the end of a response, among others. Those names belong to the provider and can change. Your browser should depend on a smaller, stable set of events that you own.
Rank #2
- [Convenient Setup] Plug and play recording USB microphone for PC, with 5.9-Foot USB cable included for computer PC laptop, is connected directly to USB-A port for recording music, computer singing or podcast. The office condenser microphone for computer is easy to use and install. (NOT compatible with Xbox and Phones)
- [Durable Metal Design] Solid sturdy metal construction design, the computer microphone for Zoom meetings with stable tripod stand is convenient when you are doing voice overs or livestreams on YouTube. Durable material extends the service life of the voice-over microphone.
- [Mic Volume Knob] Gaming condenser USB mic compatible for PS4 with additional volume knob itself has a louder or quieter adjustment and is more sensitive. Your voice would be heard well enough through the zoom microphone USB when gaming, skyping or voice recording. Also, you can adjust your volume to zero and protect your privacy.
- [Widely Use] USB-powered design, the condenser microphone for recording no need the 48v Phantom power supply, works well with Cortana, Discord, voice chat and voice recognition. The podcast microphone for Mac, with USB-B to USB-A/C cable, is compatible with desktop, laptop or PS4/PS5, which meets most of your daily recording needs.
- [Clear Output Voice] Cardioid condenser microphone for PC captures your voice properly, producing clear smooth and crisp sound. Great computer recording mic for gamers/streamers/youtubers focus on the main source and reduces background noise. The streaming microphone does the job well for broadcast ,OBS and teamspeak.
| Application event | Sent when | Data |
|---|---|---|
delta |
The upstream model emits a text increment | {"text": "..."} |
done |
The upstream model reports successful completion | {} |
error |
The upstream call fails or the server aborts the response | {"message": "..."}, with a message safe for the user |
Mapping provider events to these names in one place means a provider change touches one method, not the browser.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Server implementation
- Expose a POST endpoint that returns an emitter. The prompt must travel in a request body, so the endpoint uses
POST. Set the media type on the mapping so the response is declared as an event stream.@PostMapping(value = "/api/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter stream(@RequestBody ChatRequest request) { SseEmitter emitter = new SseEmitter(180_000L); // 3-minute ceiling emitter.onTimeout(() -> upstreamCancel(request.id())); emitter.onError(ex -> upstreamCancel(request.id())); streamExecutor.execute(() -> relay(request, emitter)); return emitter; } - Relay upstream events on a worker thread. Do not block the request thread. Read each upstream event, translate it, and send it to the emitter.
private void relay(ChatRequest request, SseEmitter emitter) { try { modelClient.streamResponse(request.prompt(), event -> { switch (event.type()) { case "response.output_text.delta" -> send(emitter, "delta", new Delta(event.delta())); case "response.completed" -> { send(emitter, "done", Map.of()); emitter.complete(); } default -> { /* ignore events the browser does not need */ } } }); } catch (Exception ex) { send(emitter, "error", Map.of("message", "The answer could not be completed.")); emitter.completeWithError(ex); } } private static void send(SseEmitter emitter, String name, Object data) { try { emitter.send(SseEmitter.event().name(name).data(data)); } catch (IOException e) { throw new UncheckedIOException(e); } } record Delta(String text) {}The
modelClientis your own wrapper around the provider SDK. Your provider’s streaming call and its event types will differ in detail, so check the event schema for the version you use. The important point is that the switch translates provider events into the contract above.Rank #3
SaleLogitech Creators Blue Yeti USB Microphone for PC, Mac, Gaming, Recording, Streaming, Podcasting, Studio and Computer Condenser Mic with Blue VO!CE effects, 4 Pickup Patterns, Plug and Play - Blackout- Custom three-capsule array: This professional USB mic produces clear, powerful, broadcast-quality sound for YouTube videos, Twitch game streaming, podcasting, Zoom meetings, music recording and more
- Blue VO!CE software: Elevate your streamings and recordings with clear broadcast vocal sound and entertain your audience with enhanced effects, advanced modulation and HD audio samples
- Four pickup patterns: Flexible cardioid, omni, bidirectional, and stereo pickup patterns allow you to record in ways that would normally require multiple mics, for vocals, instruments and podcasts
- Onboard audio controls: Headphone volume, pattern selection, instant mute, and mic gain put you in charge of every level of the audio recording and streaming process
- Positionable design: Pivot the mic in relation to the sound source to optimize your sound quality thanks to the adjustable desktop stand and track your voice in real time with no-latency monitoring
- Treat the stream as incomplete until
donearrives. A closed connection is not proof of success. The server sendsdoneonly after the upstream model reports completion, and the browser must treat a stream that ends without it as a partial answer. - Set a bounded timeout and clean up in every path. The timeout passed to the constructor limits how long one response can stay open. Cancel the upstream call in
onTimeout,onError, andonCompletionso that a departed client does not keep generating tokens you will never show. - Send heartbeat comments if your path needs them. The Servlet API does not notify the application immediately when a client disconnects. Writes are what reveal the disconnect. A comment-only heartbeat such as
: keep-aliveboth keeps idle intermediaries from closing the connection and gives the server a regular write that fails when the client has gone. Choose an interval shorter than the shortest idle timeout on the path. The sources do not give a universal value. - Size the executor deliberately. Configure a bounded pool for async MVC work instead of relying on defaults. The Spring reference documentation notes that the default async executor is not suitable for production under load. Each active stream holds a worker thread for its whole duration, so pool size should follow the number of simultaneous streams you expect.
@Configuration class AsyncConfig implements WebMvcConfigurer { @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor(); pool.setCorePoolSize(32); pool.setMaxPoolSize(64); pool.setQueueCapacity(0); pool.setThreadNamePrefix("chat-stream-"); pool.initialize(); configurer.setTaskExecutor(pool); configurer.setDefaultTimeout(180_000L); } }The numbers above are placeholders for illustration. Set them from your own load expectations and memory budget. With a zero-capacity queue, a request that arrives when all threads are busy is rejected, and you should map that rejection to an HTTP 503 response rather than letting requests pile up.
Browser client
The native EventSource API can only make GET requests, and it cannot send a request body. Because the prompt travels in a POST body, the browser reads the stream with fetch and a stream reader. The parsing rules are the same as for EventSource.
Rank #4
- 360 Degree Position Adjustable Gooseneck Design --Plug and play USB microphone Pick up the sound from 360-degree with high sensitivity, in the best possible location for sound to your PC gaming, dragon voice dictation, and talk to Cortana
- Mute Button & LED Indicator --One-click to mute/unmute your microphone for pc, Build-in LED indicator tells you the working status at any time
- Intelligent Noise-Canceling Tech --Premium omnidirectional condenser microphone with noise-canceling technology can pick up your clear voice and reduce background noise and echo
- USB Plug&Play(1.8/6ft USB Cable) -- No driver required. Just need to plug & play for the microphone to start recording, well compatible with Windows(7, 8, 10 and 11) and macOS. (NOT compatible with Xbox/Raspberry Pi/Android)
- Solid Construction--Adopting premium metal pipe and heavy-duty ABS stand to make sure that you will be satisfied with our computer mic quality
async function streamChat(prompt, onDelta) {
const res = await fetch("/api/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "text/event-stream" },
body: JSON.stringify({ prompt }),
});
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let finished = false;
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let end;
while ((end = buffer.indexOf("nn")) !== -1) {
const raw = buffer.slice(0, end);
buffer = buffer.slice(end + 2);
const outcome = handleEvent(raw, onDelta);
if (outcome === "done") finished = true;
if (outcome === "error") throw new Error("Answer failed on the server.");
}
}
if (!finished) throw new Error("The connection closed before the answer finished.");
}
function handleEvent(raw, onDelta) {
let name = "message";
const data = [];
for (const line of raw.split("n")) {
if (line === "" || line.startsWith(":")) continue;
const i = line.indexOf(":");
const field = i === -1 ? line : line.slice(0, i);
let value = i === -1 ? "" : line.slice(i + 1);
if (value.startsWith(" ")) value = value.slice(1);
if (field === "event") name = value;
else if (field === "data") data.push(value);
}
const payload = data.length ? JSON.parse(data.join("n")) : {};
if (name === "delta") onDelta(payload.text);
return name;
}
The client keeps a buffer because a network chunk can end in the middle of a message. It only appends text when a complete message (terminated by a blank line) is present. The function returns the event name so the caller can tell done from an error. The example assumes the server writes n line endings, which is what Spring writes; if you put a proxy in front that rewrites line endings, normalize them before splitting.
Free tools Windows power users keep installed
One-click scans. No signup required.
Because fetch does not reconnect automatically, a dropped stream is not resumed. Show the partial text, mark it as interrupted, and let the user retry the whole prompt.
Best Value
- 【Crystal Clear Audio Quality】Our Omnidirectional pattern condenser microphone accurately captures your voice, making it perfect for dictation, online classrooms, and more.
- 【Active Noise-Cancelling】Come in CMTECK CCS2.0 SMART CHIP with Omnidirectional Polar Pattern, which can effectively block the background noise. The pop filter prevents plosives from overloading the microphone, ensuring only your voice is heard.7
- 【Convenient Mute Button with LED Indicator】You can quickly mute/un-mute the microphone with the Mute Button and the built-in LED light lets you know the working status(Greenlight: Connected; Red light: Mute mode).
- 【Easy to use】 No drivers needed, just plug and record without external power supply, directly connect the microphone to a USB compatible device, well compatible with Windows(7, 8 and 10), Mac OS and PS4 (NOT compatible with Raspberry Pi/Linux/Android)
- 【Mini size with Adjustable Gooseneck】Adopted flexible and adjustable gooseneck metal pipe, easily adjust position 360 degrees to suit user comfort. The compact and stable base maximizes your desktop space.
Completion, errors, and disconnects
Each path ends with a different signal. Keep them distinct in the UI.
| What happened | What the server does | What the browser should show |
|---|---|---|
| Model completed | Sends done, then complete() |
Final text, no spinner |
| Upstream call failed mid-answer | Sends error, then completes the emitter with the error |
Partial text plus an error message |
| Response timeout reached | Upstream cancelled in onTimeout |
Partial text and a retry option |
| User closed the page | Upstream cancelled once a write fails or onCompletion fires; nothing is shown |
Not applicable |
| Proxy or network cut the connection | Writes fail; cleanup runs | Stream ends without done, treated as interrupted |
An error after the response has started cannot change the HTTP status. The status is already 200 by the time the model fails, so the failure has to travel as an error event. Clients that only check the status code will miss it.
Troubleshooting
- Text arrives in one large block at the end. An intermediary is buffering the response. The sources collected for this article do not establish how any particular proxy behaves, so verify through the production stack. For nginx, the usual fixes are
proxy_buffering off;for the streaming location or the response headerX-Accel-Buffering: no. - The stream stops after a fixed period of silence. An idle timeout on a load balancer, proxy, or server is closing the connection. Add heartbeat comments at a shorter interval, or raise the timeout on that hop.
- Requests return 503 or queue under load. The worker pool is saturated. Each open stream holds a thread, so increase capacity or reduce the number of simultaneous streams per user.
- The UI shows “connection closed” after every answer. The client is not recognizing
done. Check that the event name in the server matches the one inhandleEvent. - Upstream usage continues after the user leaves. The cancellation hook is missing or the upstream client ignores cancellation. Confirm that the upstream SDK exposes a cancel or close method and call it from the emitter callbacks.
Versions and compatibility
Pin the versions you use and verify them against your build rather than against this article. The Spring Framework reference documentation consulted here is the 6.2 reference page, which states that Spring Framework 7.0.9 is the latest stable release at the time of checking. This article does not establish which Spring Boot or Java version a given project uses, so check the SseEmitter and AsyncSupportConfigurer Javadoc for your release. The OpenAI Responses streaming guide is the reference for the event names used above, and those names may change over time, so confirm them against the guide before shipping. The WHATWG standard governs the wire format and is stable across the versions discussed here.
Quick Recap
Deployment checklist
- Endpoint is
POSTwithproduces = text/event-stream. - Upstream events are mapped to
delta,done, anderrorin one place. - Emitter timeout, upstream cancellation, and cleanup run for every end state.
- Heartbeat interval is shorter than every idle timeout between browser and server.
- Async executor is bounded, sized for concurrent streams, and rejects overflow with 503.
- Proxy buffering is disabled for the streaming path, and this is tested through the production stack.
- The browser treats a stream without
doneas interrupted and offers a retry.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




