How Rich's Progress Handles Concurrent Tasks and Refresh Rates

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 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 lines 610‑618, the __init__ method constructs this relationship:

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 lines 22‑40, 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 lines 335‑410. 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 lines 64‑88, with its core loop in lines 76‑89. 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

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

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()

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

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, 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), 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.

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 →