[BUG] UI freeze (120s+) due to unbounded screen buffer rendering on long sessions

Resolved 💬 2 comments Opened Feb 7, 2026 by chemica-tan Closed Feb 7, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Claude Code CLI becomes completely unresponsive — frozen UI, no cursor blink, all keyboard input ignored including Ctrl+C and Esc — during sessions with accumulated conversation history. The freeze lasts from 17 seconds to over 2 minutes, scaling with conversation length. In the worst case (--continue on a long session), the freeze is indefinite and requires process kill.

Debug log analysis points to synchronous full-screen re-rendering of an unbounded virtual screen buffer (observed up to 1,632 rows / 116 KB per frame) blocking the Node.js event loop.

Key evidence from debug logs:

  • The virtual screen buffer grows monotonically with conversation length (15 → 1,632 rows observed)
  • Every render is a full redraw: blit=0, 100.0% writes (zero incremental/differential rendering)
  • The final render frame before a forced kill wrote 116 KB in a single frame (screen=1632x120)
  • During freezes, SSD is saturated but CPU cores are idle (I/O-bound, not CPU-bound)
  • Ctrl+C does not work because the event loop is blocked and cannot dispatch signals

What Should Happen?

  • The UI should remain responsive during and after loading conversation history
  • Ctrl+C should always work (signal handling should not depend on render completion)
  • Screen rendering should be viewport-limited (only render the visible ~40 terminal rows, not 1,632 rows)
  • --continue on a long session should load without freezing

Error Messages/Logs

No error messages — the process silently freezes. The debug log shows a gap (no output at all) during the freeze.

**120s freeze in a fresh session** (debug file `f26ae9db`):

2026-02-07T17:27:28.191Z [DEBUG] Forked agent [prompt_suggestion] finished: 1 messages, types=[assistant], totalUsage: input=3 output=20 cacheRead=47960 cacheCreate=592
                          ^^^ 120 SECONDS OF COMPLETE SILENCE IN DEBUG LOG ^^^
2026-02-07T17:29:27.883Z [DEBUG] LSP Diagnostics: getLSPDiagnosticAttachments called
2026-02-07T17:29:28.305Z [DEBUG] Getting matching hook commands for UserPromptSubmit with query: undefined
2026-02-07T17:29:28.324Z [DEBUG] High write ratio: blit=0, write=1065 (100.0% writes), screen=36x120


**Final frame before kill on `--continue` session** (debug file `bf001f6e`):

2026-02-07T17:25:22.203Z [DEBUG] High write ratio: blit=0, write=116313 (100.0% writes), screen=1632x120
2026-02-07T17:25:22.528Z [DEBUG] [API:request] Creating client, ANTHROPIC_CUSTOM_HEADERS present: false, has Authorization header: false
2026-02-07T17:25:22.528Z [DEBUG] [API:auth] OAuth token check starting
2026-02-07T17:25:22.528Z [DEBUG] [API:auth] OAuth token check complete
                          ^^^ DEBUG LOG ENDS HERE (process killed by user) ^^^


**Screen buffer growth within a single session** (session f26ae9db):

17:27:00  screen= 15x120  write=   907 B   (MCP init)
17:29:28  screen= 36x120  write= 1,065 B   (post-freeze)
17:29:50  screen= 56x120  write= 2,056 B
17:30:10  screen= 74x120  write= 2,689 B
17:30:20  screen=102x120  write= 3,617 B
17:30:21  screen=161x120  write= 4,749 B
17:30:34  screen=172x120  write= 5,144 B
          ... unbounded growth continues ...
bf001f6e  screen=1632x120 write=116,313 B   (killed session)


**All freeze gaps end with the same pattern**: silence → `LSP Diagnostics: getLSPDiagnosticAttachments called` → normal activity resumes. This is consistent with a blocked event loop: timer callbacks (LSP diagnostics) are the first to fire when the loop unblocks.

Steps to Reproduce

Confirmed scenario (indefinite freeze, requires kill):

  1. Use Claude Code heavily in a single project over multiple sessions (this environment has 265 session files totaling 1.6 GB)
  2. Run a session with 100+ messages and multiple MCP servers active (4 in this case: Serena, Cipher, Tavily, Playwright)
  3. Exit the session normally
  4. Run claude --continue
  5. Wait for MCP initialization to complete (~7 s)
  6. Observe: UI freezes completely — no cursor blink, no response to any keyboard input including Ctrl+C and Esc
  7. Only recovery is to kill the process externally

Also observed (temporary freeze, self-resolves):

  1. Start a fresh session (no --continue) in a project with many accumulated sessions
  2. After startup, the UI freezes for ~120 s before accepting input
  3. During the session, additional freezes (17–61 s) occur during response streaming, with duration scaling with message count

Reproducibility: The --continue freeze was confirmed across 3 consecutive attempts, requiring kill each time.

Workaround: Start fresh sessions without --continue. Shorter freezes during streaming are unavoidable but self-resolve.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.34

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Windows Terminal

Additional Information

Environment Details

| Item | Value |
|------|-------|
| CPU | Intel Core i5-14500 (14 physical / 20 logical cores, none saturated during freeze) |
| RAM | 64 GB |
| Storage | NVMe SSD (saturated during freeze — continuous r/w band visible in Task Manager) |
| Node.js | v24.4.1 |
| MCP servers | 4 active: Serena, Cipher, Tavily, Playwright |
| Deferred tools | 52 registered (0 loaded at freeze time) |
| Permission rules | 285 total (88 user + 197 local; local rules = 51,221 chars in a single debug log line) |
| Conversation storage | 1.6 GB across 265 session files |
| Largest session files | 391 MB, 171 MB, 137 MB, 101 MB, 89 MB |

Root Cause Analysis

Primary: Synchronous full-screen re-rendering blocks the event loop

  1. Claude Code uses Ink (React for CLI) to render the terminal UI
  2. The virtual screen buffer grows with conversation length (observed: 15 → 1,632 rows)
  3. Every re-render writes the ENTIRE buffer to stdout (blit=0, no differential rendering)
  4. A 116 KB synchronous write blocks the Node.js event loop
  5. While blocked: signals are not processed (Ctrl+C ignored), input is not read, timers do not fire

Why --continue makes it worse:
--continue loads full conversation history and renders it immediately, creating a large screen buffer from the start. Fresh sessions build up gradually, giving the user brief usable windows between progressively longer stalls.

Why SSD is saturated but CPU is not:
The bottleneck is I/O. Large terminal writes (up to 116 KB/frame) combined with .claude.json persistence and JSONL appends saturate the storage subsystem.

Contributing factors:

  • blit=0 consistently — the diff algorithm finds nothing to reuse, possibly due to cursor animation or status bar changes invalidating the entire buffer
  • 285 permission rules (51 KB single log line) — unclear if this affects the render path
  • 265 accumulated session files (1.6 GB) — may slow session index operations

Freeze Gaps Observed Across Sessions

| Session | Gap | Screen rows | Write bytes | Trigger |
|---------|-----|-------------|-------------|---------|
| f26ae9db (fresh) | 120 s | 36 (after) | 1,065 | prompt_suggestion completion |
| f26ae9db (fresh) | 61 s | — | — | Post-response render |
| f26ae9db (fresh) | 22 s | — | — | Stream start (msg 79→80) |
| f26ae9db (fresh) | 20 s | — | — | Stream start (msg 169→170) |
| f26ae9db (fresh) | 17 s | — | — | Stream start (msg 135→136) |
| bf001f6e (normal use) | 155 s | — | — | Mid-session |
| bf001f6e (normal use) | 132 s | — | — | Mid-session |
| bf001f6e (normal use) | 72 s | 852 | 29,171 | Mid-session |
| bf001f6e (--continue) | indefinite | 1,632 | 116,313 | Killed by user |

Suggested Improvements

  1. Viewport-only rendering: Only render the visible terminal viewport (~40 rows), not 1,632 rows
  2. Incremental rendering: Investigate why blit=0 — stable history should be diffable
  3. Chunked/async terminal writes: Yield to event loop between write chunks to keep Ctrl+C responsive
  4. --continue lazy rendering: Load data but render only the tail; collapse older messages
  5. Screen buffer size cap: Hard-limit at ~500 rows; virtualize beyond that
  6. Loading feedback: Show a progress indicator if any phase takes >2 s

Data Available for Engineering

Debug log files and session JSONL files are available on request for cross-referencing with server-side logs. Session IDs referenced in this report:

  • f26ae9db-6637-4e79-9b4a-20ebef40fc2b (fresh session with 120 s freeze)
  • bf001f6e-bfcb-447f-a3a6-b2e3b44754c4 (long session, --continue indefinite freeze)
  • 5febb5f9-f71e-492f-8cdf-b9a547ec411e (killed after 4 s)
  • 0e32f6ea-357e-48d9-883a-499336560464 (killed after 8 s)

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗