{"record":{"id":"56f96af4672240a7","repo":"vercel/ai","slug":"the-streamable-ui-has-been-slow-to-update-this-ma","errorCode":null,"errorMessage":"The streamable UI has been slow to update. This may be a bug or a performance issue or you forgot to call `.done()`.","messagePattern":"The streamable UI has been slow to update\\. This may be a bug or a performance issue or you forgot to call `\\.done\\(\\)`\\.","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/rsc/src/streamable-ui/create-streamable-ui.tsx","lineNumber":73,"sourceCode":"function createStreamableUI(initialValue?: React.ReactNode) {\n  let currentValue = initialValue;\n  let closed = false;\n  let { row, resolve, reject } = createSuspendedChunk(initialValue);\n\n  function assertStream(method: string) {\n    if (closed) {\n      throw new Error(method + ': UI stream is already closed.');\n    }\n  }\n\n  let warningTimeout: NodeJS.Timeout | undefined;\n  function warnUnclosedStream() {\n    if (process.env.NODE_ENV === 'development') {\n      if (warningTimeout) {\n        clearTimeout(warningTimeout);\n      }\n      warningTimeout = setTimeout(() => {\n        console.warn(\n          'The streamable UI has been slow to update. This may be a bug or a performance issue or you forgot to call `.done()`.',\n        );\n      }, HANGING_STREAM_WARNING_TIME_MS);\n    }\n  }\n  warnUnclosedStream();\n\n  const streamable: StreamableUIWrapper = {\n    value: row,\n    update(value: React.ReactNode) {\n      assertStream('.update()');\n\n      // There is no need to update the value if it's referentially equal.\n      if (value === currentValue) {\n        warnUnclosedStream();\n        return streamable;\n      }\n","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/rsc/src/streamable-ui/create-streamable-ui.tsx#L55-L91","documentation":"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.","triggerScenarios":"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.","commonSituations":"Generative-UI server actions that forget to close the stream on error paths; long data fetches; early returns in action handlers.","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."],"exampleFix":"// before\nconst ui = createStreamableUI();\nui.append(await renderSlowComponent());\n// after\nconst ui = createStreamableUI();\ntry {\n  ui.append(await renderSlowComponent());\n} finally {\n  ui.done();\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"const ui = createStreamableUI();\ntry {\n  ui.append(await build());\n} catch (e) {\n  ui.error(e);\n  throw e;\n} finally {\n  ui.done();\n}","preventionTips":["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"],"tags":["streaming","rsc","dev-warning","resource-leak"],"backgroundTag":"stream-not-closed","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}