# How witr Implements Auto-Refresh and Adaptive Refresh Rates in Its TUI

> Discover how witr uses a Bubble Tea tick driver and adaptive logic for auto-refresh and dynamic refresh rates in its TUI, optimizing data updates between 3 and 30 seconds.

- Repository: [Pranshu Parmar/witr](https://github.com/pranshuparmar/witr)
- Tags: internals
- Published: 2026-08-09

---

**witr employs a Bubble Tea-based tick driver combined with performance-aware adaptive logic to automatically refresh process and port data every 3 seconds by default, dynamically adjusting the cadence between 3 and 30 seconds based on how long each refresh operation takes.**

The terminal user interface (TUI) in the `pranshuparmar/witr` repository continuously monitors system processes, network ports, and containers while intelligently balancing data freshness against CPU usage. This article examines how witr implements auto-refresh and adaptive refresh rates in its TUI by analyzing the timing constants, tick generation mechanism, and performance-based interval adjustment algorithms found in the source code.

## Core Components of witr's Auto-Refresh System

The implementation spans three architectural layers defined across [`internal/tui/constants.go`](https://github.com/pranshuparmar/witr/blob/main/internal/tui/constants.go), [`internal/tui/model.go`](https://github.com/pranshuparmar/witr/blob/main/internal/tui/model.go), and [`internal/tui/update.go`](https://github.com/pranshuparmar/witr/blob/main/internal/tui/update.go). **Timing constants** establish baseline and boundary values, a **periodic tick driver** maintains the event loop heartbeat, and **adaptive cadence logic** measures refresh duration to adjust future intervals dynamically.

## Base Refresh Interval and Timing Constants

In [`internal/tui/constants.go`](https://github.com/pranshuparmar/witr/blob/main/internal/tui/constants.go), witr defines the foundational timing parameters that govern refresh behavior. These constants establish a 3-second baseline similar to the Unix `top` command, while providing headroom for adaptive expansion up to 30 seconds.

```go
// internal/tui/constants.go
const (
    // Base auto-refresh cadence (mirrors `top` default)
    refreshInterval = 3 * time.Second
    // Adaptive-refresh parameters
    maxRefreshInterval = 30 * time.Second
    refreshStep        = 3 * time.Second
    backoffStreak      = 2
    slowFraction       = 0.6
    fastFraction       = 0.3
)

```

The `slowFraction` (0.6) and `fastFraction` (0.3) thresholds determine when a refresh is considered expensive or cheap relative to the current interval, while `backoffStreak` (2) prevents rapid oscillation by requiring consecutive observations before adjustment.

## The Tick Driver: Generating Periodic Updates

The `waitTick` function in [`internal/tui/update.go`](https://github.com/pranshuparmar/witr/blob/main/internal/tui/update.go) leverages Bubble Tea's `tea.Tick` primitive to generate periodic `tickMsg` messages. This driver maintains a constant 3-second heartbeat regardless of the current adaptive interval, delegating the actual refresh decision to separate logic.

```go
// internal/tui/update.go
func waitTick() tea.Cmd {
    return tea.Tick(refreshInterval, func(t time.Time) tea.Msg {
        return tickMsg(t)
    })
}

```

This function returns a `tea.Cmd` that produces a `tickMsg` after the base interval, ensuring the TUI remains responsive to user input while keeping the timer logic non-blocking.

## Adaptive Refresh Rate Logic

When a tick arrives, the system must decide whether to execute expensive data collection operations. This decision relies on the `refreshDue` method and the `adjustRefreshInterval` function to implement a feedback loop that responds to system load.

### Deciding When to Refresh: The refreshDue Method

The `handleTick` method in [`internal/tui/update.go`](https://github.com/pranshuparmar/witr/blob/main/internal/tui/update.go) checks `refreshDue` before invoking `refreshProcesses()` or similar data fetchers. This gatekeeper prevents overlapping refreshes and respects the current adaptive interval stored in `m.refreshEvery`.

```go
// internal/tui/update.go
func (m MainModel) handleTick(msg tickMsg) (tea.Model, tea.Cmd) {
    var cmd tea.Cmd
    if m.state == stateList && !m.quitting && ... && m.refreshDue() {
        m.lastRefresh = time.Now()
        m.refreshStartedAt = m.lastRefresh
        cmd = m.refreshProcesses()
        // refresh other tabs as needed …
    }
    return m, tea.Batch(cmd, waitTick())
}

```

The `refreshDue` method implements the primary gating logic by checking both in-flight status and elapsed time:

```go
// internal/tui/update.go
func (m MainModel) refreshDue() bool {
    // Skip if a previous refresh is still in-flight (but not older than max interval)
    if !m.refreshStartedAt.IsZero() && time.Since(m.refreshStartedAt) < maxRefreshInterval {
        return false
    }
    every := m.refreshEvery
    if every < refreshInterval {
        every = refreshInterval
    }
    // Trigger when the elapsed time since the last successful refresh exceeds the current cadence
    return time.Since(m.lastRefresh) >= every
}

```

### Measuring Performance and Adjusting Cadence

After a refresh completes, `adjustRefreshInterval` analyzes the elapsed time against the current interval. The function uses streak counters to avoid oscillation, requiring two consecutive slow or fast refreshes before modifying the interval.

```go
// internal/tui/update.go
func adjustRefreshInterval(interval, took time.Duration, slow, fast int) (time.Duration, int, int) {
    switch {
    case took > time.Duration(float64(interval)*slowFraction):
        // Refresh took longer than `slowFraction` → count a slow streak
        slow, fast = slow+1, 0
        if slow >= backoffStreak {
            interval += refreshStep
            if interval > maxRefreshInterval {
                interval = maxRefreshInterval
            }
            slow = 0
        }
    case took < time.Duration(float64(interval)*fastFraction):
        // Refresh was fast → count a fast streak
        fast, slow = fast+1, 0
        if fast >= backoffStreak {
            interval -= refreshStep
            if interval < refreshInterval {
                interval = refreshInterval
            }
            fast = 0
        }
    default:
        // Within the "stable" band → reset streaks
        slow, fast = 0, 0
    }
    return interval, slow, fast
}

```

The adaptive algorithm follows these rules:

- **Slow refreshes**: If a refresh takes longer than 60% of the current interval, increment the slow streak. After two consecutive slow refreshes, increase the interval by 3 seconds (up to the 30-second maximum).
- **Fast refreshes**: If a refresh completes in less than 30% of the current interval, increment the fast streak. After two consecutive fast refreshes, decrease the interval by 3 seconds (down to the 3-second minimum).
- **Stable zone**: Refreshes between 30% and 60% of the interval reset both streaks, maintaining the current cadence.

## End-to-End Refresh Flow

The complete lifecycle of a refresh cycle involves five coordinated stages:

1. **Initialization**: `InitialModel` seeds `refreshEvery` with the 3-second `refreshInterval` constant defined in [`internal/tui/model.go`](https://github.com/pranshuparmar/witr/blob/main/internal/tui/model.go).
2. **Tick Generation**: `waitTick` emits a `tickMsg` every 3 seconds via Bubble Tea's command system.
3. **Gating**: `handleTick` invokes `refreshDue` to check if sufficient time has elapsed since the last refresh and no operation is currently in-flight.
4. **Execution**: Upon approval, `refreshProcesses()` or equivalent functions run in the background, with `refreshStartedAt` tracking the start time.
5. **Adjustment**: When the background command returns, the code calculates `took := time.Since(m.refreshStartedAt)` and calls `adjustRefreshInterval` to potentially modify `m.refreshEvery` for the next cycle.

## Practical Implementation Details

Developers interacting with witr's codebase can manually trigger refreshes or inspect the current adaptive interval through the model's exposed fields.

To force an immediate refresh regardless of the adaptive timer:

```go
case "r":
    // Force a refresh of the current tab
    return m, m.refreshProcesses()

```

To display the current refresh cadence for debugging:

```go
case "d": // Show debug info
    m.statusMsg = fmt.Sprintf("Refresh interval: %v", m.refreshEvery)
    return m, nil

```

## Summary

- witr uses **Bubble Tea's `tea.Tick`** in [`internal/tui/update.go`](https://github.com/pranshuparmar/witr/blob/main/internal/tui/update.go) to generate a constant 3-second heartbeat that drives the UI update loop.
- The **adaptive refresh algorithm** measures each refresh duration and adjusts the interval between 3 and 30 seconds based on performance, using 60% and 30% thresholds to detect slow or fast operations.
- **Streak counters** prevent rapid oscillation by requiring two consecutive slow or fast refreshes before modifying the interval.
- The `refreshDue` method in [`internal/tui/update.go`](https://github.com/pranshuparmar/witr/blob/main/internal/tui/update.go) acts as a gatekeeper, preventing overlapping refreshes and respecting the current adaptive cadence.
- All timing parameters are centralized in [`internal/tui/constants.go`](https://github.com/pranshuparmar/witr/blob/main/internal/tui/constants.go), making the system configurable for different performance characteristics.

## Frequently Asked Questions

### What is the default refresh interval in witr?

witr defaults to a 3-second refresh interval, defined by the `refreshInterval` constant in [`internal/tui/constants.go`](https://github.com/pranshuparmar/witr/blob/main/internal/tui/constants.go). This value mirrors the default behavior of the Unix `top` command and serves as both the initial value and the minimum bound for the adaptive refresh system.

### How does witr prevent UI jank during expensive refreshes?

witr prevents UI jank through two mechanisms: the `refreshDue` method checks `refreshStartedAt` to ensure only one refresh runs at a time, and the adaptive algorithm increases the refresh interval (up to 30 seconds) when consecutive refreshes take longer than 60% of the current interval. This backoff strategy reduces CPU load during periods of high system activity.

### Can users manually trigger a refresh in witr?

Yes, users can manually trigger refreshes by pressing `r` (or `Ctrl+r` depending on key binding configuration), which invokes `m.refreshProcesses()` directly. This bypasses the adaptive timer for immediate data updates while still feeding the duration into the `adjustRefreshInterval` function to influence future automatic refreshes.

### What are the minimum and maximum refresh rates in witr?

The minimum refresh rate is 3 seconds (`refreshInterval`) and the maximum is 30 seconds (`maxRefreshInterval`), both defined in [`internal/tui/constants.go`](https://github.com/pranshuparmar/witr/blob/main/internal/tui/constants.go). The adaptive algorithm adjusts the interval in 3-second steps (`refreshStep`) within these bounds based on measured refresh performance.