for Go services
Two lines, and the query has a caller.
The install here is explicit — a middleware, a driver wrap, and two lines at the top of the functions worth measuring — because Go has no build step to hook and no way to reach the caller's context without being handed it. What you get for those two lines is the thing the rest of this depends on: a query with a function above it, so an N+1 has somewhere to land.
What it measures
- Every function you mark
- ctx, span := sixty.Start(ctx, "orders.List") and a deferred capture. Two lines, and the context has to be threaded through — which is the price of there being no ambient context in Go, and the reason the number is trustworthy when it does arrive.
- Every query, on any database/sql driver
- The agent wraps the driver rather than the connection, so pgx, lib/pq, MySQL and anything else registered against database/sql are all measured — and every optional interface the real driver implements is mirrored, so nothing it could do stops working.
- Rows, counted as they are read
- Not from a returned slice, because database/sql has none. The span closes when the rows close, which is what makes the count real rather than inferred.
- Which function issued which query
- The context carries the current span, so a query is credited to the function that issued it. This is what turns "the endpoint got slow" into "this function started making twenty calls".
- Your HTTP routes, in and out
- sixty.Middleware wraps anything that speaks net/http — chi, gorilla, echo, the Go 1.22 pattern mux. Where the router knows a pattern the path does not, sixty.SetRoute names it.
What it finds
Every one of these is compared against the release before it, so the finding names the change rather than the day.
An N+1 that was not there last release
A function that made one database call per request now makes twenty. Deploy-anchored, so the finding names the release rather than the hour.
A query that started reading the whole table
Rows per call went from 30 to 30,000 — and here that count is taken as the caller iterates, so it is what your code actually read rather than what the statement might have returned.
Your own code getting slower
Time in the function itself, separated from time in what it called, so you open the right file on the first try.
Something that is now called in a loop
Same speed per call, same data, called hundreds of times where it used to be called once.
A query that quietly returns nothing
No error, no slow call, and empty where it used to have rows. Nothing in a Go service reports this as a failure, because nothing failed.
Errors, grouped by where they came from
By code location rather than by message — one bug is one entry however many values it formats in.
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-go ships on purpose, against the dataset its seed command creates — about 1,000 users and 30,000 orders. You can run it and watch them arrive.
orders.GetUserOrdersA refactor dropped the WHERE clause, so every call pulls the whole table and filters it in Go. Latency barely moved on a warm database — the row count is what gives it away, and it is real because it is taken as the rows are read.
since release v2, against v1
orders.EnrichOrdersA single batched query became a per-order lookup. Each one is fast and indexed, so nothing in a latency graph moves — only the count.
since release v2, against v1
How it installs
Hand it to the coding agent you already have open. It finds main(), the handler and the sql.Open, threads the context where it needs threading, and marks the functions worth measuring — which is the step that is easy to describe and tedious to do by hand.
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:
go get github.com/sixty-sh/sixty-go
// main(), before the server starts
defer sixty.Init(sixty.Config{}).Shutdown(context.Background())
// requests — anything that speaks net/http
http.ListenAndServe(":8080", sixty.Middleware(mux))
// queries — the driver you already use
db, err := sixty.Open("pgx", dsn)
// functions — the ones worth measuring
func (s *Store) GetUserOrders(ctx context.Context, id int64) (_ []Order, err error) {
ctx, span := sixty.Start(ctx, "orders.GetUserOrders")
defer span.Capture(&err)
...
}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
- The context has to be threaded. A goroutine that is not passed the ctx does its work outside the operation — correct for background work, wrong for a fan-out you meant to measure. There is no ambient-context version of this and we will not build one, because the version that guesses is the version that attributes one request's queries to another request.
- Query plans are not captured. That is the finding that names a cause rather than a symptom — "this stopped using its index" — and on a Go service it does not arrive.
- CPU and waiting are not split. Goroutines migrate between threads, so a per-thread clock does not describe a span, and reporting one would be a number that is wrong exactly when it matters.
- Postgres is what the detector understands in depth. Another database registered against database/sql still reports timings, rows and counts; the statement-level conclusions are weaker.
- Nothing here marks your functions for you. Skipping that step leaves the feed with routes and queries and nothing in between, which is the half that makes the rest worth reading.
If one of these is why you are here
Find out what your last change did.
Try it!Building something else? See Node services, Python services or Ruby and Rails apps — those pages are different, and so is what we can see.