How Windows Terminal Uses Virtual Memory for the Text Buffer: A Deep Dive

Windows Terminal reserves a contiguous virtual address space for the text buffer and commits physical memory on demand in batches of 128 rows, reducing the private working set by roughly 70% compared to upfront allocation.

Windows Terminal manages massive text buffers—often thousands of rows by 80+ columns—without bloating the process's memory footprint. Instead of relying on the general-purpose heap, the terminal leverages virtual memory to reserve address space and commit physical pages only when rows are actually accessed. This approach keeps the private working set minimal while supporting theoretically unlimited scrollback.

Why Virtual Memory Instead of Heap Allocation

Traditional heap allocation via new or malloc would force the terminal to either pre-allocate the entire buffer upfront—wasting physical RAM for unused rows—or suffer from heap fragmentation as the buffer grows. By using VirtualAlloc directly, Windows Terminal decouples address space reservation from physical memory commitment. This allows the buffer to claim a massive contiguous address range (enough for the maximum configured rows) while consuming actual RAM only for rows that contain data.

Reserving the Virtual Address Space

When a TextBuffer is constructed, it immediately reserves the full virtual address space needed for the worst-case scenario. This reservation does not consume physical memory; it merely prevents the address range from being allocated for other purposes.

In src/buffer/out/textBuffer.cpp, the constructor uses VirtualAlloc with the MEM_RESERVE flag:

// src/buffer/out/textBuffer.cpp
// We use explicit virtual memory allocations to not taint the general purpose allocator …
_buffer = wil::unique_virtualalloc_ptr<std::byte>{
    static_cast<std::byte*>(THROW_LAST_ERROR_IF_NULL(
        VirtualAlloc(nullptr, allocSize, MEM_RESERVE, PAGE_READWRITE)))
};

(see textBuffer.cpp lines 112–113)

The _buffer member, a wil::unique_virtualalloc_ptr, holds the base address of this reserved region. At this stage, the process working set remains unchanged; no pages are mapped to physical RAM.

On-Demand Commit with Read-Ahead Batching

Physical memory is committed only when the terminal actually writes to a row. To minimize expensive system calls, Windows Terminal does not commit pages one at a time. Instead, it uses a read-ahead batching strategy.

The 128-Row Batch Strategy

When a row access misses the committed region, the _commit method is invoked. This method commits a block of rows defined by the constant _commitReadAheadRowCount, set to 128 rows by default. This amortizes the cost of VirtualAlloc across many row operations.

In src/buffer/out/textBuffer.cpp, the commit logic looks like this:

// src/buffer/out/textBuffer.cpp
THROW_LAST_ERROR_IF_NULL(VirtualAlloc(_commitWatermark, size, MEM_COMMIT, PAGE_READWRITE));

(see textBuffer.cpp line 40)

Tracking the Commit Watermark

The TextBuffer class tracks the boundary between reserved and committed memory using two raw pointers declared in src/buffer/out/textBuffer.hpp:

  • _buffer – Points to the start of the reserved virtual address space.
  • _commitWatermark – Points to the first byte after the last committed page. The range [ _buffer , _commitWatermark ) contains fully constructed ROW objects ready for use.
  • _bufferEnd – Points to the end of the reserved region, marking the absolute limit of the buffer.

(see textBuffer.hpp lines 55–71)

When _getRow is called with an index beyond the watermark, the buffer commits the next batch of rows and advances _commitWatermark accordingly.

De-commit and Buffer Reset

When the terminal clears the screen or resets the buffer (for example, during a cls command or a RIS sequence), the committed pages are returned to the system to reduce physical memory pressure. However, the virtual address space reservation is retained so the buffer can grow again without requiring a new VirtualAlloc reservation.

The _decommit method in textBuffer.cpp calls VirtualFree with the MEM_DECOMMIT flag:

_textBuffer->_decommit();          // frees committed pages
// The reserved arena remains; a later write will re-commit as needed.

This ensures that a long-running terminal session does not accumulate physical RAM for scrollback history that has been discarded.

Memory Savings in Practice

The virtual memory strategy yields significant memory efficiency. For a typical buffer configuration of 80 columns and 9,000 rows:

  • Upfront commit: Would consume approximately 7 MiB of physical RAM immediately.
  • On-demand commit: Starts with roughly 2 MiB of resident memory, growing only as the buffer fills with actual content.

This ~70% reduction in initial working set allows Windows Terminal to open multiple tabs and panes without exhausting system memory, while still supporting massive scrollback buffers when needed.

Summary

  • Windows Terminal uses VirtualAlloc with MEM_RESERVE to claim a contiguous address space for the text buffer without consuming physical RAM.
  • Memory is committed on demand via MEM_COMMIT in batches of 128 rows to amortize system call overhead.
  • The _commitWatermark pointer tracks the boundary between reserved and committed regions, ensuring efficient row access.
  • Buffer resets use MEM_DECOMMIT to release physical pages while preserving the virtual reservation for future growth.
  • This strategy reduces the initial working set by approximately 70% compared to upfront allocation.

Frequently Asked Questions

Why doesn't Windows Terminal use malloc for the text buffer?

Using malloc or new would either force the terminal to pre-allocate the entire buffer upfront—wasting physical RAM for empty rows—or cause heap fragmentation as the buffer grows dynamically. By using VirtualAlloc directly, the terminal separates address space reservation from physical memory commitment, keeping the process working set minimal while maintaining a contiguous, expandable buffer.

What happens when the terminal scrolls past committed rows?

When _getRow is called with an index beyond the current _commitWatermark, the text buffer automatically triggers the _commit method. This commits the next batch of 128 rows (by default) using VirtualAlloc with MEM_COMMIT, advances the watermark pointer, and returns the requested row. This happens transparently during normal scrolling operations.

How does the 128-row batch size affect performance?

The constant _commitReadAheadRowCount = 128 amortizes the cost of kernel transitions across multiple row accesses. Committing memory one row at a time would require a VirtualAlloc system call for every new line displayed, causing significant overhead during rapid output. By committing 128 rows at once, the terminal reduces system call frequency while slightly increasing the immediate memory footprint—a trade-off optimized for typical console workloads.

Can the virtual memory reservation fail on 32-bit systems?

Yes. The text buffer attempts to reserve a contiguous region large enough for the maximum configured rows and columns. On 32-bit systems, the available virtual address space is limited to 2–4 GiB, and heavy fragmentation can make it difficult to find a large contiguous block. If VirtualAlloc with MEM_RESERVE fails, the terminal cannot create the buffer and will report an out-of-memory error, though this is rare on modern 64-bit systems.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →