# How Rich's Progress Handles Concurrent Tasks and Refresh Rates

> Learn how Rich Progress manages concurrent tasks and refresh rates. Discover its Live rendering system, thread-safe Task objects, and configurable refresh parameters for smooth, efficient display.

- Repository: [Textualize/rich](https://github.com/Textualize/rich)
- Tags: internals
- Published: 2026-03-06

---

**Rich's Progress uses a Live rendering system with a background refresh thread to display multiple concurrent tasks safely, using thread-safe Task objects and configurable refresh rates via `refresh_per_second` and `update_period` parameters.**

The `Progress` class in the [Textualize/rich](https://github.com/Textualize/rich) library provides a powerful way to visualize long-running operations in the terminal. Understanding how it manages concurrent task updates and controls screen refresh rates is essential for building responsive CLI applications that handle parallel workloads without blocking the main thread.

## The Live Rendering Foundation

`rich.progress.Progress` is built on top of the **Live** rendering system. When you instantiate `Progress`, it creates a `Live` object internally to drive screen updates.

In [`rich/progress.py`](https://github.com/Textualize/rich/blob/main/rich/progress.py) lines [610‑618](https://github.com/Textualize/rich/blob/master/rich/progress.py#L610-L618), the `__init__` method constructs this relationship:

```python
self.live = Live(
    self.get_renderable(),
    refresh_per_second=refresh_per_second,
    ...
)

```

The `refresh_per_second` parameter (defaulting to **10**) controls how often the display redraws. This value is passed directly to `Live`, which spawns a background **_RefreshThread**. According to the source in [`rich/live.py`](https://github.com/Textualize/rich/blob/main/rich/live.py) lines [22‑40](https://github.com/Textualize/rich/blob/master/rich/live.py#L22-L40), this thread wakes every `1 / refresh_per_second` seconds and calls `live.refresh()` to repaint the current renderable.

## Thread-Safe Task Architecture

Each progress bar is represented by a **Task** dataclass defined in [`rich/progress.py`](https://github.com/Textualize/rich/blob/main/rich/progress.py) lines [335‑410](https://github.com/Textualize/rich/blob/master/rich/progress.py#L335-L410). This structure tracks:

- `task_id`: Unique identifier
- `description`: Text label
- `total` and `completed`: Progress counters
- Timing fields for speed estimation

Tasks are stored in the `self._tasks` dictionary. All mutations to task state are protected by an **RLock**, making the system thread-safe for concurrent updates from multiple Python threads.

## Concurrent Updates with `_TrackThread`

The `Progress.track()` helper provides automatic progress advancement when iterating over sequences. When `auto_refresh` is enabled, `track()` spawns a background **_TrackThread** to monitor progress without blocking your code.

The `_TrackThread` implementation resides in [`rich/progress.py`](https://github.com/Textualize/rich/blob/main/rich/progress.py) lines [64‑88](https://github.com/Textualize/rich/blob/master/rich/progress.py#L64-L88), with its core loop in lines [76‑89](https://github.com/Textualize/rich/blob/master/rich/progress.py#L76-L89). This thread:

1. Sleeps for `update_period` (default **0.1 seconds**)
2. Checks if the task's `completed` counter has changed
3. Calls `self.progress.advance(task_id, ...)` when updates occur
4. Forces a final `refresh=True` update when the task completes

This design allows the main thread to focus on computation while the background thread handles UI updates at a throttled rate.

## Managing Multiple Concurrent Tasks

Because each `Task` lives independently in `self._tasks`, you can create many simultaneous progress bars using `add_task()`. The Live render always shows the current values of **all** tasks whenever it refreshes.

When using `track()` or manual `advance()` calls from different threads, each thread updates only its associated `TaskID`. The background `_RefreshThread` aggregates these changes into a single coherent display, ensuring no partial or corrupted renders appear on screen.

## Configuring Refresh Rates

Two independent parameters control the update granularity:

- **`refresh_per_second`**: Passed to `Progress` and forwarded to `Live`, this sets the maximum frequency of screen redraws (default **10 Hz**). Lower values reduce terminal flicker and CPU usage.

- **`update_period`**: Passed to `track()` or `_TrackThread`, this controls how often the background thread checks task progress and issues `advance()` calls (default **0.1 s**).

If `auto_refresh` is disabled (`auto_refresh=False`), the caller must manually trigger redraws using `progress.refresh()` or pass `refresh=True` to `update()`.

## Practical Examples

### Single Task with Default Refresh

```python
from rich.progress import Progress
import time

with Progress() as prog:
    task = prog.add_task("Downloading", total=100)
    for i in range(100):
        prog.advance(task, 1)
        time.sleep(0.05)

```

### Multiple Concurrent Tasks with Threading

```python
from rich.progress import Progress
import threading, time, random

def worker(prog, task_id):
    for _ in range(random.randint(20, 60)):
        prog.advance(task_id, 1)
        time.sleep(random.random() * 0.1)

prog = Progress()
task_a = prog.add_task("[red]Task A", total=50)
task_b = prog.add_task("[green]Task B", total=70)

threading.Thread(target=worker, args=(prog, task_a)).start()
threading.Thread(target=worker, args=(prog, task_b)).start()

prog.start()
while not prog.finished:
    time.sleep(0.1)
prog.stop()

```

### Custom Refresh Rates with `track()`

```python
from rich.progress import Progress
import time

items = range(200)
with Progress(refresh_per_second=5) as prog:
    for item in prog.track(items, description="Processing", update_period=0.2):
        time.sleep(0.02)

```

### Manual Refresh Control

```python
from rich.progress import Progress
import time

prog = Progress(auto_refresh=False)
task = prog.add_task("Manual", total=10)
prog.start()

for i in range(10):
    prog.advance(task, 1)
    prog.refresh()
    time.sleep(0.3)

prog.stop()

```

## Summary

- **Rich Progress** delegates rendering to a **Live** container that refreshes the screen at a configurable rate via `_RefreshThread`.
- **Task** objects are stored in a thread-safe dictionary with RLock protection, enabling safe concurrent updates.
- The **`track()`** method uses **_TrackThread** to advance progress in the background without blocking the main loop.
- **`refresh_per_second`** controls UI redraw frequency, while **`update_period`** throttles how often progress values are checked.
- For fine-grained control, disable `auto_refresh` and call `refresh()` manually to batch updates and reduce terminal flicker.

## Frequently Asked Questions

### Is Rich Progress thread-safe for concurrent updates?

Yes. According to the source code in [`rich/progress.py`](https://github.com/Textualize/rich/blob/main/rich/progress.py), all task mutations occur inside an **RLock**. You can safely call `advance()` or `update()` from multiple Python threads simultaneously, and the Live render will display consistent state during its next refresh cycle.

### What's the difference between `refresh_per_second` and `update_period`?

**`refresh_per_second`** controls how often the terminal display redraws (managed by `Live` in [`rich/live.py`](https://github.com/Textualize/rich/blob/main/rich/live.py)), while **`update_period`** controls how often the background `_TrackThread` checks if a task has progressed. These operate independently: you can check for updates frequently while redrawing the screen slowly to reduce flicker.

### How do I disable automatic refreshing to reduce flicker?

Pass `auto_refresh=False` when creating the `Progress` instance. This prevents the background `_RefreshThread` from running. You must then manually call `prog.refresh()` or pass `refresh=True` to `update()` whenever you want the screen to repaint, allowing you to batch multiple progress changes into a single redraw.

### Can I mix manual updates with `track()` in the same Progress instance?

Yes. You can create some tasks using `add_task()` and update them manually from worker threads, while using `track()` for others. The Live render aggregates all tasks in `self._tasks` during each refresh cycle, regardless of how they were created or updated.