for Python services
Was it your code, or was it waiting?
Every other agent can tell you that a function got slower. This one tells you which half of it — the code that was computing, or the time it spent waiting for a lock, a pool slot or a C extension holding the GIL. Those need opposite fixes, and every other per-call number stays correct while the second one happens, which is why nothing else catches it.
What it measures
- CPU per call
- time.thread_time() is per-thread and costs about 100ns to read, and a synchronous span owns its thread for its whole duration — so this is an exact number rather than a share of a process-wide clock apportioned between whatever else was running.
- Waiting per call
- The rest of a function's own time: a lock held across more work than it used to be, a connection pool with no free slot, a C extension that took the GIL and did not give it back. It is invisible to every other measurement here because the code is not slower — it is queueing.
- Every psycopg query
- Versions 2 and 3, patched at the cursor rather than at the connection, so SQLAlchemy sitting on top of psycopg is measured too. Rows returned, columns, response size, and the normalized statement.
- Which function issued which query
- A query is credited to the function above it, so an N+1 lands on the function with the loop in it rather than being spread across a query that always looked fine on its own.
- Your HTTP routes, as they are written
- Flask by the matched url_rule, Django by the route as it appears in urls.py, and anything ASGI or WSGI by what you wrap. So the feed says GET /users/<int:user_id>/orders rather than one operation per user id.
What it finds
Every one of these is compared against the release before it, so the finding names the change rather than the day.
Your own code getting slower, split in two
The finding that names which fix to reach for. Time computing went up and time waiting did not: look in the function. Time waiting went up and CPU did not move: look at the pool, the lock, or whatever the process is now sharing a thread with.
An N+1 that was not there last release
A function that made one query per request now makes twenty. Each one is fast and correctly indexed, so no timing moves enough to notice — only the count does.
A query that started reading the whole table
Rows per call went from 30 to 30,000. On a warm database with development-sized data this barely moves the clock, and it takes production down when there is a year of rows behind it.
Something that is now called in a loop
Unchanged per call — same speed, same data — and called hundreds of times where it used to be called once.
A query that quietly returns nothing
Succeeded, raised nothing, was not slow, and came back empty where it used to come back with rows. Most of the ways a service like this breaks make the numbers smaller rather than larger.
Errors, grouped by where they came from
By code location rather than by message, so one bug is one entry however many different strings it formats into.
What lands in your feed
Not a dashboard to read. One card per finding, with the numbers, the release that changed them, and the evidence your coding agent needs to write the fix.
These are the two regressions apps/demo-python ships on purpose, against the dataset its seed script creates — about 1,000 users and 30,000 orders. You can run it and watch them arrive.
orders.get_user_ordersA refactor dropped the WHERE clause, so every call pulls the whole table and filters it in Python. The clock barely moved; the row count is the entire signal.
since release v2, against v1
orders.enrich_ordersA batched lookup became a per-order one. Every query is individually fast and correctly indexed, which is exactly what lets this survive review and staging.
since release v2, against v1
How it installs
Hand it to the coding agent you already have open. It works out whether this project is Flask, Django, FastAPI or a bare WSGI callable, puts the middleware in the right place, and marks the service layer — then fixes what gets found, in the same conversation.
claude mcp add sixty \
-e SIXTY_API_KEY=sixty_sk_… \
-e SIXTY_ENDPOINT=https://ingest.sixty.sh \
-- npx -y @sixty-sh/mcp
# or by hand:
pip install sixty-sh
# wsgi.py, asgi.py or main.py — before any connection is opened
import sixty
sixty.init()
# then one of these, whichever this project is:
# Flask instrument_flask(app)
# Django SixtyMiddleware, first in MIDDLEWARE
# ASGI SixtyASGIMiddleware
# WSGI SixtyMiddleware around the callable
# and the code between the view and the database
@sixty.trace
def get_user_orders(user_id):
...The release identifier is picked up on its own from Vercel, Render, Railway, Fly, Heroku or GitHub Actions, and SIXTY_RELEASE sets it anywhere else. The first comparison arrives after your next deploy, because there is nothing to compare one release against.
What it does not do
- A span that sits across an await shares its thread with whatever else the loop ran, so it reports no CPU rather than an inflated one. An asyncio service gets CPU per synchronous function and per query, but not per request — the honest answer rather than a plausible one.
- Query plans are not captured. Postgres is there and the EXPLAIN would work; this agent does not issue one yet, so "it stopped using its index" arrives on Node and Ruby and not here.
- psycopg is the only driver instrumented. SQLAlchemy on psycopg is measured, because the cursor underneath it is — asyncpg and the MySQL drivers are not measured at all.
- Under gunicorn or uwsgi each worker reports independently and the collector merges them. That is correct, and worth knowing when you read a number that describes one process.
- Nothing here marks your functions for you. Node gets that from a build transform and Python has no equivalent hook, so the decorator or instrument_module is a step you take — and skipping it leaves the feed with routes and queries and nothing in between.
If one of these is why you are here
Find out what your last change did.
Try it!Building something else? See Node services, Go services or Ruby and Rails apps — those pages are different, and so is what we can see.