# How Lazygit Manages Memory When Handling Large Diffs: PTY Streaming and Incremental Reading

> Discover how Lazygit efficiently manages memory for large diffs using PTY streaming and incremental reading. Keep UI performance high by reading only visible lines.

- Repository: [Jesse Duffield/lazygit](https://github.com/jesseduffield/lazygit)
- Tags: internals
- Published: 2026-03-02

---

**Lazygit streams large diffs through a pseudo-terminal (PTY) and reads only the lines visible in the UI viewport, keeping memory usage proportional to the screen size rather than the total diff size.**

When working with massive repositories in jesseduffield/lazygit, users often encounter diffs spanning thousands of lines. Rather than loading entire patches into RAM, lazygit implements a sophisticated memory management strategy that processes diffs incrementally. This approach leverages pseudo-terminal streaming and viewport-aware reading to maintain consistent performance regardless of diff size.

## The PTY-Based Streaming Architecture

Lazygit avoids buffering complete diff outputs by executing git commands inside a pseudo-terminal and consuming the output as a stream.

### Building Diff Commands with DiffHelper

The process begins in [`pkg/gui/controllers/helpers/diff_helper.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/helpers/diff_helper.go), where the `DiffArgs` method constructs the git arguments (e.g., `--stat -p <ref>`). The resulting command object is created via `GitCommands.DiffCmdObj` in [`pkg/commands/git_commands/diff.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/git_commands/diff.go), which prepares the executable task without immediately running it.

```go
// Build diff arguments and create command object
args := diffHelper.DiffArgs()                     // -> ["--stat","-p","HEAD"]
cmdObj := gui.Git().Diff.DiffCmdObj(args)        // git diff command object

```

### Executing in a Pseudo-Terminal

When rendering is required, `Gui.newPtyTask` in [`pkg/gui/pty.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/pty.go) determines whether the command needs a PTY based on custom pager configuration or external diff settings. If needed, it invokes `pty.StartWithSize` to spawn the process inside a pseudo-terminal and registers the PTY file descriptor in `gui.viewPtmxMap`.

```go
// Run the diff in a PTY - lazygit decides automatically if PTY is required
gui.RenderToMainViews(gui.types.RefreshMainOpts{
    Main: &gui.types.ViewUpdateOpts{
        Title:   "Diff",
        Task:    gui.types.NewRunPtyTaskWithPrefix(cmdObj.GetCmd(),
                 style.FgMagenta.Sprintf("git diff %s\n\n", strings.Join(args, " "))),
    },
})

```

This PTY-based execution allows lazygit to treat the git output as a stream rather than a static buffer, enabling line-by-line consumption while the command runs.

## Viewport-Aware Line Reading

The core memory optimization comes from reading only what the user can actually see, plus a small buffer for scrollbar calculations.

### Calculating Visible Lines

In [`pkg/gui/view_helpers.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/view_helpers.go), the `linesToReadFromCmdTask` function determines exactly how many lines are needed to fill the current view and render an accurate scrollbar. This calculation returns a `LinesToRead` struct that instructs the UI precisely how much data to fetch from the command task.

```go
// LinesToRead struct controls how many lines are fetched
type LinesToRead struct {
    TotalLines       int
    LinesToRead      int
    ReadFromStart    bool
}

```

### Task-Based Incremental Consumption

The actual consumption happens in [`pkg/gui/tasks_adapter.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/tasks_adapter.go), where PTY output is wrapped in a task that reads incrementally via `manager.ReadLines` or `ReadToEnd`. As the user scrolls, the task fetches additional chunks on-demand rather than slurping the entire diff into memory. This ensures the memory footprint remains bounded by the viewport size plus scrollbar metadata, not the total diff size.

## Minimal State and Line Number Adjustment

Lazygit maintains almost no persistent diff data in memory. The `Diffing` struct in [`pkg/gui/modes/diffing/diffing.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/modes/diffing/diffing.go) stores only the reference and direction flags (`Ref`, `Reverse`), containing zero diff content.

When the UI must map a line number from the diff view back to the working tree file, the `adjustLineNumber` method in [`diff_helper.go`](https://github.com/jesseduffield/lazygit/blob/main/diff_helper.go) runs a minimal on-the-fly diff with `--unified=0` and parses only the necessary patch data via `patch.Parse`. This targeted approach avoids loading the full file or complete diff history into memory.

```go
// Adjust line number using minimal diff calculation
adjusted := diffHelper.adjustLineNumber(
    42,                     // line number shown in diff view
    "--",                  // diff against working copy
    "path/to/file.go",      // file path
)

```

## Summary

- **Lazygit manages memory for large diffs** by streaming git output through a pseudo-terminal (PTY) rather than buffering the entire output.
- **Viewport-limited reading** via `LinesToRead` calculations in [`pkg/gui/view_helpers.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/view_helpers.go) ensures only visible lines plus a small scrollbar buffer reside in memory.
- **Task-based incremental consumption** in [`pkg/gui/tasks_adapter.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/tasks_adapter.go) reads diff output line-by-line as the user scrolls, preventing memory spikes.
- **Minimal state storage** means the `Diffing` struct holds only metadata (reference and direction), not diff content.
- **Efficient line mapping** uses targeted `--unified=0` diffs via `adjustLineNumber` to calculate positions without parsing full patches.

## Frequently Asked Questions

### Does lazygit load the entire diff into memory when viewing large patches?

No. According to the jesseduffield/lazygit source code, the program explicitly avoids loading complete diffs. Instead, it streams the `git diff` output through a pseudo-terminal and reads only the amount of data needed to fill the visible viewport plus a small buffer for scrollbar calculations.

### How does lazygit handle custom pagers with large diffs?

Lazygit supports custom pagers through `PagerConfig` in [`pkg/config/pager_config.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/config/pager_config.go). When a custom pager is configured, lazygit still wraps the pager command inside the same PTY infrastructure used for standard diffs. The pager may handle its own buffering, but lazygit itself never stores the full diff before handing it to the pager.

### What happens to memory usage when scrolling through a massive diff?

Memory usage remains stable and proportional to the terminal height. As implemented in [`pkg/gui/tasks_adapter.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/tasks_adapter.go), scrolling triggers incremental reads via the task manager (`ReadLines`), fetching additional chunks only when the viewport moves. Previously read lines may be discarded or cached based on the specific view implementation, but the total resident memory does not scale with the diff size.

### How does lazygit calculate line numbers without parsing the full diff?

The `adjustLineNumber` function in [`pkg/gui/controllers/helpers/diff_helper.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/helpers/diff_helper.go) executes a minimal diff with `--unified=0` on-demand, parsing only the specific hunk headers required to map the displayed line number back to the working tree file. This targeted approach uses `patch.Parse` on a tiny subset of data rather than the complete diff output.