# Share a pyinstrument Report — The Call Tree, Not a Terminal Paste

Canonical: https://commareports.com/share-pyinstrument-report
Published: 2026-09-08

> pyinstrument --html writes a self-contained call tree you can only open locally. Publish it to Comma for a URL colleagues can expand, search and comment on.

# Share a pyinstrument report

pyinstrument's terminal output is excellent for the person who ran it and
useless for everybody else. It collapses to your terminal width, hides frames
under its own significance threshold, and turns into an unreadable block of
box-drawing characters the moment it is pasted into Slack.

The HTML renderer is the shareable one:

```bash
pyinstrument --html -o profile.html manage.py some_slow_command
```

or, in code, around the part you actually care about:

```python
from pyinstrument import Profiler

profiler = Profiler(async_mode="enabled")
profiler.start()
run_the_slow_thing()
profiler.stop()

with open("profile.html", "w") as f:
    f.write(profiler.output_html())
```

That file is self-contained — the viewer and the profile data are both inlined
— which makes it easy to produce and awkward to deliver. It is too big to paste,
it downloads instead of opening when attached to a ticket, and Slack will not
preview it.

## Publish it

Drag `profile.html` into [the app](https://commareports.com/), or send it to the
API:

```bash
curl -fsS -X POST "https://commareports.com/api/v1/reports" \
  -H "Authorization: Bearer $COMMA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html profile.html \
        --arg t "pyinstrument — nightly export command" '{title: $t, html: $html}')"
```

Report HTML renders with scripts enabled inside a sandboxed iframe
(`allow-scripts`, no `allow-same-origin`), so the collapsible tree, the
timeline/call-stack toggle, and the "hide irrelevant frames" control all work
for the reader — including the branches you did not expand before sending it.

## Profiling a Django or FastAPI request

The common case is not a script, it is one slow endpoint. pyinstrument ships
middleware for exactly this:

```python
# settings.py
MIDDLEWARE = ["pyinstrument.middleware.ProfilerMiddleware", *MIDDLEWARE]
PYINSTRUMENT_PROFILE_DIR = "profiles"
```

Every request with `?profile` writes an HTML profile into `profiles/`. Publish
the one that matters rather than trying to describe it:

```bash
curl -fsS -X PATCH "https://commareports.com/api/v1/reports/$REPORT_ID" \
  -H "Authorization: Bearer $COMMA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --rawfile html "$(ls -t profiles/*.html | head -1)" \
        --arg t "GET /api/orders — 4.2s p99" '{title: $t, html: $html}')"
```

PATCHing one report id keeps a single URL per endpoint and appends a revision
per capture, so "did the index actually help?" is a
[diff](/docs/ci) rather than a memory.

## What review adds

- **Anchored threads** on the expensive frame — see
  [commenting on HTML](/comment-on-html).
- **Revisions**, so before-and-after is one artifact with one link.
- **Access per report** — a profile exposes your internal module layout, so
  private, team or named-reviewer access is the right default. See the
  [sharing model](/docs/sharing).

## Limits

- **Entry HTML: 5 MB.** pyinstrument bundles its viewer into every file, so a
  short profile is already a few hundred KB; long captures of async code with
  many distinct stacks are the ones to watch.
- **60 requests/minute per token.**

## Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision
history.

**[Publish a profile →](https://commareports.com/)**

### Related

- [Share a py-spy profile](/share-py-spy-profile) · [Share a Scalene report](/share-scalene-report)
- [Share a memray report](/share-memray-report) · [Share a flame graph](/share-flamegraph)
- [Share a profiling report](/share-profiling-report) · [Share a benchmark report](/share-benchmark-report)
