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:

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, or POST it:

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:

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.

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 →

Related