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 identifierdescription: Text labeltotalandcompleted: 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:
- Sleeps for
update_period(default 0.1 seconds) - Checks if the task's
completedcounter has changed - Calls
self.progress.advance(task_id, ...)when updates occur - Forces a final
refresh=Trueupdate 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 toProgressand forwarded toLive, this sets the maximum frequency of screen redraws (default 10 Hz). Lower values reduce terminal flicker and CPU usage. -
update_period: Passed totrack()or_TrackThread, this controls how often the background thread checks task progress and issuesadvance()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_secondcontrols UI redraw frequency, whileupdate_periodthrottles how often progress values are checked.- For fine-grained control, disable
auto_refreshand callrefresh()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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →