Docs
Runpeek docs: live links for Python progress bars
Runpeek streams a Python script's tqdm progress bars, output and exit code to a live link that opens on any device. Install it with pip, start your script with tqdml run and share the link. Live links are free and need no account; they expire 30 minutes after the last update, and a free account keeps runs for 7 days.
Quickstart
You need Python 3.8 or newer. Install the client from PyPI:
pip install tqdm-liveThen start your script through the tqdml wrapper. Nothing in your code has to change:
tqdml run -- python train.pyBefore your script starts, the wrapper prints the run's link:
runpeek ▶ https://runpeek.app/r/8c1f4b2e-5d7a-4e9b-a3c6-0f2d9e8b7a14Open it on your phone or laptop, or send it to a teammate. Every tqdm bar in the script shows up there with the script's output and, once it ends, its exit code. Your terminal keeps showing everything as before.
Or add it in code
Swap the import to stream a single loop:
from tqdm_live import tqdm
for batch in tqdm(loader, desc="train"):
train(batch)Or patch tqdm once at startup to stream every bar in the process, including bars that libraries create with tqdm:
import tqdm_live
tqdm_live.patch()
from tqdm.auto import tqdm # every tqdm bar now streamsWithout a run, each bar prints its own /b/ link. Use tqdml run or start_run() to collect bars, output and the exit code under one /r/ link.
What you see
- Runs
- A run link (
/r/...) shows the run's name, status (Running, Finished or Failed), duration, exit code, host and last update, then every progress bar in the run and its output. - Bars
- A bar link (
/b/...) shows one bar's percentage, count, rate, elapsed time and ETA. Paste more bar links into the page to watch several side by side; the page's link keeps all of them. - Output
tqdml runstreams stdout and stderr about once a second and marks stderr lines. Live links keep the last 500 lines; saved runs keep 500 (Free) or 10,000 (Pro), and you can load earlier lines on the page.- Exit codes
tqdml runrecords your command's exit code and exits with it: 0 ends the run as Finished, anything else as Failed, and Ctrl+C as Failed with 130. Runs started in code end as Finished when Python exits, or as Failed with exit code 1 when an exception escapes.- Quiet bars
- A running bar without an update for 10 seconds shows No updates. Slow steps are normal; it doesn't mean the script died.
CLI reference
Installing tqdm-live adds the tqdml command.
tqdml run [-n NAME] [--no-logs] -- COMMAND- Runs
COMMANDas one run and prints its link. Bars the command creates attach to the run without code changes.-n,--namesets the run name (default: the command line, up to 120 characters).--no-logskeeps stdout and stderr out of the run; bars and the exit code still stream. Put--before your command so its own flags go to it. tqdml login [--no-browser] [--endpoint URL]- Connects this machine to your account. It shows a short code and opens the approval page in your browser; approve it while signed in and the client saves a token.
--no-browseronly prints the link, for servers and SSH sessions. tqdml logout- Removes the saved token from this machine. Revoke the token in your dashboard to stop it everywhere.
tqdml whoami [--endpoint URL]- Prints the email and plan your runs are saved to.
Python API
tqdm_live.tqdm- A drop-in subclass of
tqdm.tqdm. Same arguments, and it streams. tqdm_live.patch()- Makes every bar built on
tqdm.stdstream:tqdm.tqdm,trangeandtqdm.autooutside notebooks. Jupyter widgets fromtqdm.notebookaren't covered. Calling it twice is harmless. start_run(name=None) -> str | None- Starts a run, prints its link and returns its ID; bars created afterwards join it. The name defaults to your script's file name. Returns
Nonewhen streaming is disabled. end_run(status="finished", exit_code=0)- Ends the active run as
"finished"or"failed". You rarely need it: runs end as Finished when Python exits and as Failed when an exception escapes. notify(url, events=("finished", "failed"), stall_after_minutes=None) -> bool- Adds a webhook to the active run, so call it after
start_run(). Discord and Slack webhook URLs get a formatted message; any other URL receives a JSON event. Passstall_after_minutesto also hear about a run that stops updating. ReturnsTruewhen the webhook was added. setup(endpoint=None, min_interval=None, link_base=None, token=None)- Sets only
endpoint,min_interval,link_baseandtoken, from code. Call it before your first bar.
import tqdm_live
from tqdm_live import tqdm
tqdm_live.start_run("resnet50 sweep")
tqdm_live.notify("https://discord.com/api/webhooks/...", stall_after_minutes=15)
for epoch in tqdm(range(90), desc="epochs"):
train_one_epoch()Environment variables
| Variable | What it does | Default |
|---|---|---|
TQDMLIVE_ENDPOINT | Server the client sends to | https://runpeek.app |
TQDMLIVE_LINK_BASE | Base URL of the printed links | The endpoint |
TQDMLIVE_MIN_INTERVAL | Minimum seconds between updates of one bar | 0.75 |
TQDMLIVE_DISABLE | 1, true or yes stops streaming; bars still draw in your terminal | Off |
TQDMLIVE_NOTIFY | Comma-separated webhook URLs added to every run | None |
RUNPEEK_TOKEN | API token that saves runs to your account | None |
Saving runs to your dashboard
Live links work without an account. A free account keeps runs in your dashboard for 7 days and adds alert rules and API tokens. Sign in, then connect your machine:
tqdml loginFrom then on, runs from this machine are saved to your dashboard. That includes scripts that never call start_run(): with a token, the first bar starts a run named after the script. Notebooks and interactive sessions are skipped, so they don't turn into one endless run.
If a run can't be saved, it still streams live and the client prints one line saying why: the token was rejected, or you've used this month's saved runs.
CI and servers
Create an API token in your dashboard and pass it as RUNPEEK_TOKEN. A token can save runs and identify its account; it can't read your runs or change settings.
# .github/workflows/eval.yml
jobs:
eval:
runs-on: ubuntu-latest
env:
RUNPEEK_TOKEN: ${{ secrets.RUNPEEK_TOKEN }}
steps:
- uses: actions/checkout@v4
- run: pip install tqdm-live
- run: tqdml run -n "nightly eval" -- python eval.pyThe client uses the first token it finds: setup(token=...), then RUNPEEK_TOKEN, then the login saved by tqdml login. A saved login is only sent to the server that issued it. It lives in ~/.config/runpeek/config.json (%APPDATA%\runpeek\config.json on Windows), readable only by your user.
Notifications
Per-run webhooks
Any run can ping up to five webhooks when it finishes or fails, and when it goes quiet for a number of minutes you choose (1 to 1,440). Add them on the run page, with notify() in code, or for every run with TQDMLIVE_NOTIFY.
Discord and Slack webhook URLs get a formatted message with a link to the run. Any other URL receives JSON, and each delivery is tried up to three times:
{
"event": "finished",
"run": {
"id": "8c1f4b2e-5d7a-4e9b-a3c6-0f2d9e8b7a14",
"name": "train.py",
"status": "finished",
"exit_code": 0,
"host": "gpu-box",
"started_at": 1767225600000,
"ended_at": 1767229200000
},
"link": "https://runpeek.app/r/8c1f4b2e-5d7a-4e9b-a3c6-0f2d9e8b7a14",
"sent_at": 1767229201000
}Account alert rules
With an account, alert rules cover every saved run without touching your code, for example a Slack message whenever any run fails or stalls. Free includes 3 rules; Pro includes 25 and email alerts. Targets are stored in your account and shown redacted once saved.
Plans and limits
Live links are free for everyone. Accounts add saved history: Free costs nothing, Pro is $9 a month or $90 a year. See pricing for the details.
| Feature | No account | Free | Pro |
|---|---|---|---|
| Price | $0 | $0 | $9/month or $90/year |
| Live share links | Included | Included | Included |
| Run history | 30 minutes after the last update | 7 days | 365 days |
| Saved runs per month | Not included | 1,000 | 20,000 |
| Log lines kept per run | 500 | 500 | 10,000 |
| Per-run webhooks | Up to 5 | Up to 5 | Up to 5 |
| Account alert rules | Not included | 3 | 25 |
| Email alerts | Not included | Not included | Included |
| API tokens | Not included | 10 | 50 |
Technical limits
- Bar updates are up to 16 KB of JSON. Updates to the same bar less than 150 ms apart are dropped.
- Bar descriptions keep 120 characters, units 16.
- Output arrives in batches of up to 200 lines; lines longer than 500 characters are cut.
- Each run takes up to 5 webhooks, with stall alerts from 1 minute to 24 hours.
- Anonymous runs and bars expire 30 minutes after their last update.
HTTP API
The Python client is a thin layer over a small JSON API, so anything that speaks HTTP can stream progress. You pick the IDs (random UUIDs) and send a random write key with every write.
| Endpoint | What it does |
|---|---|
POST /api/push/{bar_id} | Send a bar update |
GET /api/state/{bar_id} | Latest state of a bar, 404 once it expired |
GET /api/stream/{bar_id} | Server-sent events with every bar update |
POST /api/run/{run_id} | Send a run heartbeat: name, status, exit code |
POST /api/run/{run_id}/logs | Send output lines |
GET /api/run-state/{run_id} | Run snapshot: the run, its bars and recent output |
GET /api/run-stream/{run_id} | Server-sent events: snapshot, run, bar and logs |
GET /api/run/{run_id}/logs?before={seq}&limit={n} | Earlier output, up to 1,000 lines a page |
GET, POST /api/run/{run_id}/webhooks | List or add a run's webhooks |
DELETE /api/run/{run_id}/webhooks/{id} | Remove a webhook; send the token its POST returned as x-runpeek-webhook-token |
Push a bar
BAR_ID=$(python -c "import uuid; print(uuid.uuid4())")
KEY=$(python -c "import secrets; print(secrets.token_hex(16))")
curl -X POST "https://runpeek.app/api/push/$BAR_ID" \
-H "content-type: application/json" \
-H "x-tqdmlive-key: $KEY" \
-d '{"v":1,"id":"'"$BAR_ID"'","desc":"train","n":420,"total":1000,
"unit":"it","rate":12.5,"elapsed":33.6,"eta":46.4,"status":"running"}'
echo "https://runpeek.app/b/$BAR_ID"total, rate and eta may be null; times are in seconds. Send "status": "closed" with the last update. To put a bar in a run, add "run" (the run ID) and "run_key" (the run's write key) to its updates.
Runs and output
POST /api/run/{run_id}
x-tqdmlive-key: {run_key}
Authorization: Bearer {token} (optional, saves the run)
{
"v": 1, "kind": "run", "id": "{run_id}", "name": "train.py",
"status": "running", "exit_code": null, "host": "gpu-box", "pid": 4242,
"started_at": 1767225600000, "ended_at": null, "ts": 1767225633600
}POST /api/run/{run_id}/logs
x-tqdmlive-key: {run_key}
{
"v": 1, "kind": "logs", "id": "{run_id}", "seq": 7,
"lines": [{ "ts": 1767225634000, "s": "o", "t": "epoch 3/90 loss 0.412" }]
}Timestamps are Unix milliseconds. "s" is "o" for stdout and "e" for stderr. When you send a token, the response header x-runpeek-notice says saved, token_invalid or quota_reached.
Write keys and responses
The first write with an x-tqdmlive-key header claims the bar or run; later writes with a different key get 401, so a shared link can't be used to write. Successful writes return 204, including updates dropped for coming too fast. Invalid JSON or fields return 400 and bodies over 16 KB return 413.
Live updates
The stream endpoints send server-sent events with one JSON object per message, plus a keep-alive comment every 25 seconds. A run stream starts with a snapshot event, then sends run, bar and logs events as they happen.
Security
- A link is the key. Run and bar IDs are random UUIDs, so nobody finds a run without its link, and anyone with the link can watch it. Share links like you'd share a private document link.
- Watching can't write. Updates need the write key the client generates for each run and bar, which never appears in a link.
- Tokens can't read runs. An API token can save runs to your account but can't read runs or change settings. Revoke it in your dashboard anytime.
- Per-run webhooks belong to the link. Anyone with a run link can see its webhooks, with their URLs redacted, and add their own. Only the browser that added a webhook can remove it. For links you share widely, use account alert rules instead.
- What we store. The run name, host name, process ID, timings, status and exit code; each bar's description, counts and timings; and your command's output when you use
tqdml runwithout--no-logs. Never your code, data or environment variables. Keep secrets out of bar descriptions and output. - Turn it off anytime.
TQDMLIVE_DISABLE=1stops all sending; your bars keep drawing in the terminal.