# Share a py-spy Profile — Send the Flame Graph, Not a Screenshot

Canonical: https://commareports.com/share-py-spy-profile
Published: 2026-09-08

> py-spy record writes an interactive SVG that loses its zoom in GitHub and Slack. Publish it to Comma for a URL where the flame graph still zooms, searches and can be commented on.

# Share a py-spy profile

`py-spy` is the tool you reach for when a Python process is already
misbehaving and you cannot restart it to add instrumentation. It attaches from
the outside, samples the stacks, and writes a flame graph:

```bash
py-spy record -o profile.svg --pid 12345 --duration 30
```

The SVG it writes is interactive — click a frame to zoom, Ctrl-F to highlight
every stack containing a symbol. That interactivity is where the diagnosis
lives, and it does not survive the trip to anyone else:

- **GitHub** strips `<script>` from SVG when it renders it. You get the picture.
- **Slack** flattens it into an image preview.
- **A Jira attachment** downloads rather than opens.

So the profile ends up as a cropped screenshot with an arrow drawn on it, and
whoever reads it cannot check the stack underneath your arrow.

## Publish it

Drag `profile.svg` into [the app](https://commareports.com/), or POST it:

```bash
py-spy record -o profile.svg --pid "$(pgrep -f gunicorn | head -1)" --duration 30

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 svg profile.svg \
        --arg t "py-spy — web-3, 12:40 UTC" '{title: $t, html: $svg}')"
```

Comma renders report content with scripts enabled inside a sandboxed iframe
(`allow-scripts`, no `allow-same-origin`), which is precisely the capability
GitHub's sanitizer takes away. Zoom, reset and search all work at the URL.

Useful flags when the profile is for someone else to read:

- `--subprocesses` — include forked workers, so a Gunicorn or Celery pool shows
  up as one graph instead of one arbitrary child.
- `--idle` — include idle threads. Usually noise, occasionally the whole answer
  when the problem is that everything is blocked on one lock.
- `--native` — include C extension frames, which is how you tell "slow in numpy"
  apart from "slow in our loop around numpy".

## Profiling during an incident

Re-profiling is normal — you capture, someone changes a setting, you capture
again. PATCH the same report id rather than creating a new one:

```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 svg profile.svg \
        --arg t "py-spy — web-3, after pool bump" '{title: $t, html: $svg}')"
```

One link in the incident channel, and it stays correct as the investigation
moves. The revision list is the timeline.

## Set the access deliberately

A production Python profile contains your module paths, your third-party
dependency versions, and often enough function names to reconstruct the shape
of a service. Reports are private by default; keep incident profiles on team or
named-reviewer access rather than a public link. See the
[sharing model](/docs/sharing).

## Limits

- **Entry SVG: 5 MB.** A 30-second capture is comfortably inside it. A
  multi-minute `--subprocesses --native` capture on a large pool can approach it
  — shorten the duration rather than dropping frames.
- **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 flame graph](/share-flamegraph) · [Share a speedscope profile](/share-speedscope-profile)
- [Share a pyinstrument report](/share-pyinstrument-report) · [Share a memray report](/share-memray-report)
- [Share a Scalene report](/share-scalene-report) · [Share a profiling report](/share-profiling-report)
