Skip to content

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:

Terminal
pip install tqdm-live

Then start your script through the tqdml wrapper. Nothing in your code has to change:

Terminal
tqdml run -- python train.py

Before your script starts, the wrapper prints the run's link:

Output
runpeek ▶ https://runpeek.app/r/8c1f4b2e-5d7a-4e9b-a3c6-0f2d9e8b7a14

Open 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:

Python
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:

Python
import tqdm_live

tqdm_live.patch()

from tqdm.auto import tqdm  # every tqdm bar now streams

Without 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 run streams 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 run records 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 COMMAND as one run and prints its link. Bars the command creates attach to the run without code changes. -n, --name sets the run name (default: the command line, up to 120 characters). --no-logs keeps 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-browser only 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.std stream: tqdm.tqdm, trange and tqdm.auto outside notebooks. Jupyter widgets from tqdm.notebook aren'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 None when 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. Pass stall_after_minutes to also hear about a run that stops updating. Returns True when the webhook was added.
setup(endpoint=None, min_interval=None, link_base=None, token=None)
Sets only endpoint, min_interval, link_base and token, from code. Call it before your first bar.
Python
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

VariableWhat it doesDefault
TQDMLIVE_ENDPOINTServer the client sends tohttps://runpeek.app
TQDMLIVE_LINK_BASEBase URL of the printed linksThe endpoint
TQDMLIVE_MIN_INTERVALMinimum seconds between updates of one bar0.75
TQDMLIVE_DISABLE1, true or yes stops streaming; bars still draw in your terminalOff
TQDMLIVE_NOTIFYComma-separated webhook URLs added to every runNone
RUNPEEK_TOKENAPI token that saves runs to your accountNone

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:

Terminal
tqdml login

From 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 Actions
# .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.py

The 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:

JSON
{
  "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.

Runpeek plans compared
FeatureNo accountFreePro
Price$0$0$9/month or $90/year
Live share linksIncludedIncludedIncluded
Run history30 minutes after the last update7 days365 days
Saved runs per monthNot included1,00020,000
Log lines kept per run50050010,000
Per-run webhooksUp to 5Up to 5Up to 5
Account alert rulesNot included325
Email alertsNot includedNot includedIncluded
API tokensNot included1050

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.

EndpointWhat 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}/logsSend 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}/webhooksList 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

Shell
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

HTTP
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
}
HTTP
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 run without --no-logs. Never your code, data or environment variables. Keep secrets out of bar descriptions and output.
  • Turn it off anytime. TQDMLIVE_DISABLE=1 stops all sending; your bars keep drawing in the terminal.