vercel/ai · warning
The streamable UI has been slow to update. This may be a bug
Error message
The streamable UI has been slow to update. This may be a bug or a performance issue or you forgot to call `.done()`.
What it means
Analogous to the streamable value warning: in development mode, createStreamableUI arms a timer that fires if the UI stream hasn't been updated or closed within HANGING_STREAM_WARNING_TIME_MS. It indicates the server never called .done() (or .update()/.append() stalled), leaving the UI stream hanging.
Source
Thrown at packages/rsc/src/streamable-ui/create-streamable-ui.tsx:73
function createStreamableUI(initialValue?: React.ReactNode) {
let currentValue = initialValue;
let closed = false;
let { row, resolve, reject } = createSuspendedChunk(initialValue);
function assertStream(method: string) {
if (closed) {
throw new Error(method + ': UI stream is already closed.');
}
}
let warningTimeout: NodeJS.Timeout | undefined;
function warnUnclosedStream() {
if (process.env.NODE_ENV === 'development') {
if (warningTimeout) {
clearTimeout(warningTimeout);
}
warningTimeout = setTimeout(() => {
console.warn(
'The streamable UI has been slow to update. This may be a bug or a performance issue or you forgot to call `.done()`.',
);
}, HANGING_STREAM_WARNING_TIME_MS);
}
}
warnUnclosedStream();
const streamable: StreamableUIWrapper = {
value: row,
update(value: React.ReactNode) {
assertStream('.update()');
// There is no need to update the value if it's referentially equal.
if (value === currentValue) {
warnUnclosedStream();
return streamable;
}
View on GitHub (pinned to 69428b1f8b)
Solutions
- Call streamableUI.done() on all paths (wrap in try/finally).
- Call .update()/.append() promptly to show progress and reset the warning timer.
- Use .error(...) on failure so the client stream resolves.
- Ignore/increase the threshold if the operation is legitimately slow in dev.
Example fix
// before
const ui = createStreamableUI();
ui.append(await renderSlowComponent());
// after
const ui = createStreamableUI();
try {
ui.append(await renderSlowComponent());
} finally {
ui.done();
} Defensive patterns
Strategy: try-catch
Try / catch
const ui = createStreamableUI();
try {
ui.append(await build());
} catch (e) {
ui.error(e);
throw e;
} finally {
ui.done();
} Prevention
- Always close streamable UI with done()/error() in finally
- Reset the warning timer with update/append for long operations
- Test server actions with exceptions injected to catch unclosed streams
When it happens
Trigger: Calling createStreamableUI() in a server action/RSC handler and not calling .done() (or not updating) within the warning window; an exception thrown between creation and close; slow UI-building work exceeding the threshold.
Common situations: Generative-UI server actions that forget to close the stream on error paths; long data fetches; early returns in action handlers.
Related errors
- The streamable value has been slow to update. This may be a
- ${method}: Value stream is already closed.
- ${method}: Value stream is locked and cannot be updated.
- .append(): The current value is not a string. Received: ${ty
- .append(): The value is not a string. Received: ${typeof val
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/56f96af4672240a7.
Report an issue: GitHub.