All jobs

Investigate a Goroutine Leak in a Go Service

Yes. An agent reads your Go service and its recent commits, finds the goroutines that can block forever, and reports each with its file, line and a pprof check.

Run this on RushIn a Project that has your repository.

What the agent does

The agent clones the repository into its own cloud sandbox and starts from the shape of the metric you give it: go_goroutines climbing from 300 to 40,000 over two days, memory following, then an OOM kill. Steady growth that tracks traffic means some goroutine starts per request or per message and never returns. The agent's job is to find every place a goroutine can wait forever, on a channel nobody sends to, a network call with no deadline, or a context that is never cancelled.

In a service like evrone/go-clean-template it reads go.mod first to learn the transports: Fiber v2 for HTTP, gRPC, RabbitMQ through amqp091-go, NATS, and pgx v5 for Postgres. Then it reads the RPC plumbing in pkg/. In pkg/rabbitmq/rmq_rpc/client/client.go it notes that notify is created unbuffered at line 81, so the message loop started at line 216 blocks on c.notify <- err at line 219 until some caller reads it, and that RemoteCall starts its span from the client's own c.ctx at line 127 rather than the caller's context, so a caller's cancellation never reaches the wait in remoteCallWait. It checks how pending calls are cleaned up: addCall and the deferred deleteCall at lines 157 and 158, and the timer at line 197.

It reads every outbound call on a request path the same way. internal/repo/webapi/translation_google.go builds a new go-googletrans translator on every call at line 35 and calls trans.Translate at line 37 with no context, so the request's deadline does not reach the HTTP call underneath; if translate.google.com stalls, each Fiber handler waiting on it stays parked. The agent checks the library's HTTP client for a timeout and notes what it finds. It also reads pkg/postgres/postgres.go, where _defaultMaxPoolSize is 1 and .env.example sets PG_POOL_MAX=2, because goroutines waiting for a pool connection pile up the same way.

Last, it reads the 30 most recent commits with git log -p and marks any new go statement, channel, context.Background() on a request path, or dependency bump. Where it can, it runs the service's own tests (go test ./...) and a short load test against a handler in the sandbox while reading runtime.NumGoroutine() or /debug/pprof/goroutine?debug=2, to show which path leaves goroutines behind. It writes incident.md and changes no code.

Start it with one API call

POST https://api.prix.dev/o/{org}/sessions with this body:

{
  "agent": "codex",
  "input": "Incident: Grafana shows go_goroutines on the service rising from 300 to 40,000 over two days and memory following it, until the pod is OOM killed and restarts. Read the code, the configuration and the last 30 commits, find the most likely causes, and write incident.md with a timeline, each suspect with file and line evidence, and the check that would confirm or clear it. Change no code.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
  1. Get an API key. In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
  2. Send the request. POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
  3. Read what came back. Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

What comes back

What comes back is incident.md, attached to the session as an artifact. After a timeline of the commits in the window, each suspect gets its own section: the file and line where the goroutine starts, the line where it can block, what it is waiting for and why that may never arrive. Each one ends with the check that settles it, usually a goroutine profile from a running pod (go tool pprof on /debug/pprof/goroutine, grouped by stack) showing thousands of goroutines parked at that exact line, or a metric such as in-flight upstream calls rising with go_goroutines.

If the agent reproduced a leak in the sandbox, the report includes the test or load command and the goroutine counts before and after, marked as sandbox measurements. The session's transcript keeps every command and file read, so the evidence behind each suspect can be checked.

Before fixing anything, an engineer captures one goroutine profile from production and compares its top stacks with the report's suspects. That one profile usually confirms or clears the top suspect, and a follow-up to the same session can ask the agent to write the fix, such as passing the request context through or adding a deadline, as a pull request.

Run it on every goroutine count alert

An alert on the rate of go_goroutines (for example, growth over six hours with no matching traffic growth) can call a handler on your server that sends this request with the service name and the numbers in the prompt. Leaks grow slowly, so an alert on the trend starts the investigation a day before the OOM kill rather than after it.

Set a webhook URL on the API key and Rush POSTs run.completed when the report is ready; your handler reads the session and attaches incident.md to the alert or the incident ticket.

The use case this job follows, with its automation and trigger: Investigate an incident.

Why run it on Rush?

A goroutine leak investigation is mostly careful reading: every go statement, every channel send and receive, every outbound call and whether a context reaches it. The agent does that reading across the whole repository in its own sandbox, with the Go toolchain installed, so it can also run the tests and a small load test to watch the count grow, without touching your laptop's Go setup or the running service.

Every claim in the report points to a file and line, and every measurement to a command and its output in the session's transcript. Several services can be investigated at once, one run each, up to the key's concurrent run limit.

Frequently asked questions

Can it read a goroutine dump from production?

Yes, if you give it one. Attach the output of /debug/pprof/goroutine?debug=2 as a file in the request's input and the agent matches its stacks to the code. Without one, it reports the check that would produce it.

Does it understand errgroup, worker pools and context cancellation?

It reads them as code: where an errgroup limit is set, which goroutine owns a channel, and whether a context passed in is the one that reaches the blocking call. In go-clean-template, for example, it notes that the RabbitMQ client and servers call SetLimit(1) on their errgroups, so their message loops cannot multiply.

Will it add pprof or change my code?

No. The prompt says to change no code. If the service does not expose net/http/pprof, the report says so and suggests where to mount it, and you can ask for that change in a follow-up.