# lazygit Async Refresh and Fetch Architecture: How the TUI Stays Responsive

> Discover lazygit's async refresh and fetch architecture. Learn how its three-tier concurrency system keeps the TUI responsive by isolating Git I/O and managing goroutines.

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

---

**lazygit maintains a responsive terminal interface by isolating Git I/O through a three-tier concurrency architecture: RefreshableView enums define updatable UI panels, RefreshOptions control execution modes (SYNC, ASYNC, or BLOCK_UI), and RefreshHelper orchestrates concurrent goroutines via a background worker pool, while BackgroundRoutineMgr independently handles periodic fetches.**

Keeping a terminal UI snappy while continuously querying Git for commits, branches, and status updates requires careful concurrency management. lazygit solves this through a sophisticated async refresh and fetch architecture that delegates all heavy operations to background workers. According to the jesseduffield/lazygit source code, the system centers on the `RefreshHelper` coordinator in [`pkg/gui/controllers/helpers/refresh_helper.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/helpers/refresh_helper.go) and a dedicated `BackgroundRoutineMgr` in [`pkg/gui/background.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/background.go) that together ensure the model stays current without blocking the main thread.

## Core Architecture Components

The refresh system is built around three coordinated abstractions that separate UI concerns from Git operations.

### RefreshableView and RefreshMode

Every panel that can be updated—commits, files, branches, tags, and more—is enumerated as a `RefreshableView` constant in [`pkg/gui/types/refresh.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/types/refresh.go). This type-safe approach allows the system to target specific UI sections rather than redrawing the entire screen.

The `RefreshOptions` struct controls *how* a refresh executes via the `Mode` field, which supports three distinct behaviors:

- **SYNC** – Runs refresh tasks immediately on the calling goroutine, blocking the UI until completion.
- **ASYNC** – Dispatches each view refresh to the background worker pool via `c.OnWorker`, keeping the UI interactive.
- **BLOCK_UI** – Wraps the operation in a UI update block that ignores keybindings while redrawing.

These definitions live in [`pkg/gui/types/refresh.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/types/refresh.go) alongside the `Scope` field, which allows callers to limit updates to specific panels like `types.COMMITS` or `types.FILES`.

### RefreshHelper Orchestrator

The `RefreshHelper` struct in [`pkg/gui/controllers/helpers/refresh_helper.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/helpers/refresh_helper.go) serves as the central coordinator. Its `Refresh` method (lines 54–106) accepts `RefreshOptions`, determines which views need updates, and manages the lifecycle of concurrent refresh tasks. When operating in `ASYNC` mode, it spawns each view refresh as a separate goroutine through `utils.Safe` and synchronizes them with a `sync.WaitGroup`.

## The Refresh Workflow

A refresh moves through four distinct phases, from trigger to completion.

### 1. Trigger Events

Refreshes are initiated by keybindings, focus-gain events, or background timers. For example, when a panel gains focus, the GUI calls:

```go
gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})

```

This invokes the public façade `guiCommon.Refresh` defined in [`pkg/gui/gui_common.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/gui_common.go), which forwards the request to `RefreshHelper.Refresh`.

### 2. Mode-Based Dispatch

Inside `RefreshHelper.Refresh`, the system inspects `options.Mode` to decide execution strategy:

- **ASYNC**: Each sub-task (like `refreshCommitsAndCommitFiles` or `refreshBranches`) is handed to `c.OnWorker`, placing it on gocui’s background worker pool. A `sync.WaitGroup` tracks completion.
- **SYNC**: The helper runs sub-tasks directly and blocks until they finish.
- **BLOCK_UI**: The entire operation runs on the UI thread while ignoring input, ensuring atomic visual updates.

The helper builds a default set of views unless a specific `Scope` is provided, then spawns goroutines for each refresh function.

### 3. Concurrent Execution

Each view refresh runs in its own goroutine wrapped with `utils.Safe` for panic recovery. The implementation logs timing metrics for performance monitoring:

```go
go utils.Safe(func() {
    t := time.Now()
    defer wg.Done()
    f()
    self.c.Log.Infof("refreshed %s in %s", name, time.Since(t))
})

```

This pattern appears in [`pkg/gui/controllers/helpers/refresh_helper.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/helpers/refresh_helper.go) around lines 110–117, ensuring that expensive Git commands never execute on the main UI thread.

### 4. Post-Refresh Synchronization

After the `WaitGroup` signals completion, `refreshStatus()` updates the global status bar. If the caller provided a `Then` callback in `RefreshOptions`, it executes once all views are current.

## Background Fetch Architecture

While the refresh system updates the UI model, the background fetch system keeps repository data synchronized with remotes without user intervention.

### BackgroundRoutineMgr and Timer

The `BackgroundRoutineMgr` struct in [`pkg/gui/background.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/background.go) initializes during GUI startup. If `git.autoFetch` is enabled, `startBackgroundFetch` (lines 29–36) sets up a ticker using `goEvery` that fires every `Refresher.FetchInterval`.

### Fetch Execution Flow

When the timer fires, the `fetch` callback runs briefly on the UI thread only to record a timestamp, then immediately delegates the heavy work to a worker task via `gui.helpers.AppStatus.WithWaitingStatusImpl` or direct invocation of `backgroundFetch()`.

The `backgroundFetch` method performs two critical operations:

1. **Git Fetch**: Calls `git.Sync.FetchBackground()`, which constructs a `git fetch --no-write-fetch-head` command via the `fetchCommandBuilder` in [`pkg/commands/git_commands/sync.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/git_commands/sync.go) (lines 55–63). This suppresses output and avoids writing `FETCH_HEAD`, making it safe for silent background execution.

2. **Partial UI Refresh**: Triggers a **synchronous** refresh of volatile panels (branches, commits, remotes, tags) to guarantee the model reflects new remote data before redrawing.

### Auto-Forward and Manual Fetch

After a successful fetch, `AutoForwardBranches()` runs on the UI thread to fast-forward local branches that lag behind their upstreams when the user enables this feature. Manual fetches invoked via the `F` keybinding route through `gui.helpers.BranchesHelper.Fetch()`, which uses `SyncCommands.Fetch` for synchronous operations or `SyncCommands.FetchBackground` when delegating to the background manager.

## Interaction Between Refresh and Fetch

The two systems operate independently but share critical infrastructure.

**Fetch triggers Refresh**, but not vice versa. After every background fetch completes, the system initiates a targeted synchronous refresh to update affected views. Conversely, a manual refresh does **not** automatically trigger a fetch; they remain decoupled to prevent unnecessary network operations.

Both systems utilize the same **worker pool** accessed through `c.OnWorker`. This shared resource guarantees that heavy I/O—whether file-system walks during refresh or network operations during fetch—runs off the main UI goroutine, preserving TUI responsiveness.

## Practical Code Examples

### Trigger an Asynchronous Refresh

From within a custom controller, fire-and-forget refreshes keep the UI interactive:

```go
func (self *SomeController) reloadAll() {
    self.c.Refresh(types.RefreshOptions{
        Mode:  types.ASYNC,
        Scope: []types.RefreshableView{
            types.COMMITS,
            types.FILES,
        },
    })
}

```

This invokes `guiCommon.Refresh` in [`pkg/gui/gui_common.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/gui_common.go), which delegates to `RefreshHelper.Refresh` and ultimately spawns worker goroutines.

### Invoke a Background Fetch Manually

To run the same fetch command used by the periodic timer:

```go
func (self *FetchController) fetchNow() error {
    return self.c.Git().Sync.FetchBackground()
}

```

This executes the command builder in [`pkg/commands/git_commands/sync.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/git_commands/sync.go), creating a fetch process that writes no fetch-head and suppresses terminal output.

### Force a Synchronous Refresh

When switching repositories or requiring immediate consistency, block the UI until all panels update:

```go
gui.c.Refresh(types.RefreshOptions{
    Mode: types.SYNC,
})

```

`RefreshHelper.Refresh` detects `types.SYNC` and executes the refresh closure immediately without spawning workers, as implemented in lines 98–106 of [`pkg/gui/controllers/helpers/refresh_helper.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/controllers/helpers/refresh_helper.go).

## Summary

- **Three-tier architecture**: `RefreshableView` enums define targets, `RefreshOptions` configures behavior, and `RefreshHelper` coordinates execution.
- **Three execution modes**: `ASYNC` runs on worker pools, `SYNC` blocks the UI thread, and `BLOCK_UI` ignores input during redraw.
- **Worker pool isolation**: All expensive Git operations execute via `c.OnWorker` or `utils.Safe` goroutines to prevent UI freezing.
- **Decoupled fetch system**: `BackgroundRoutineMgr` handles periodic fetches independently, triggering targeted synchronous refreshes only after new data arrives.
- **Safe background fetches**: Uses `git fetch --no-write-fetch-head` via `SyncCommands.FetchBackground` to avoid side effects during silent updates.

## Frequently Asked Questions

### What is the difference between SYNC and ASYNC refresh modes in lazygit?

**SYNC mode** runs refresh tasks immediately on the calling goroutine, blocking the UI until all Git operations complete and views redraw. **ASYNC mode** dispatches each panel refresh to the background worker pool via `c.OnWorker`, allowing the user to continue interacting with the interface while Git queries execute concurrently. ASYNC is the default for most operations to maintain responsiveness.

### How does lazygit prevent the UI from freezing during a background fetch?

The system delegates network I/O to the worker pool through `BackgroundRoutineMgr`, which runs the fetch command in a separate goroutine via `WithWaitingStatusImpl`. The fetch itself uses `git fetch --no-write-fetch-head` constructed in [`pkg/commands/git_commands/sync.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/commands/git_commands/sync.go), and only after completion does it trigger a brief synchronous refresh of affected views on the UI thread.

### What triggers a refresh in lazygit?

Refreshes are triggered by three primary events: explicit keybindings calling `gui.c.Refresh()`, focus-gain handlers that update panels when the user switches views, and post-operation callbacks after Git commands complete. The `RefreshHelper` receives these requests through the `guiCommon.Refresh` façade and routes them according to the specified `Mode` and `Scope`.

### How does the auto-fetch feature work?

When `git.autoFetch` is enabled, `BackgroundRoutineMgr.startBackgroundFetch` initializes a ticker during GUI startup in [`pkg/gui/background.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/gui/background.go). Every `Refresher.FetchInterval`, it invokes `FetchBackground()` to silently fetch remotes, then runs a synchronous refresh of branches and commits. If enabled, `AutoForwardBranches()` subsequently fast-forwards local branches that are behind their upstreams.