httpsnoop: capturing HTTP metrics without breaking your handler
Package httpsnoop provides an easy way to capture http related metrics (i.e. response time, bytes written, and http status code) from your application's http.Handlers.
At a glance
- What is it?
- httpsnoop is a small Go package that records response time, bytes written and status code for an http.Handler by wrapping the ResponseWriter without hiding the optional interfaces that handler code depends on. It solves one narrow problem carefully, with documented limits on what it cannot wrap.
- Who is it for?
- httpsnoop suits a Go service that needs per-request metrics, latency logs or status code accounting and does not want to hand-roll a ResponseWriter wrapper, because it reproduces the exact optional interface set of the writer it replaces. It does not suit a codebase whose handlers depend on custom interfaces added on top of the standard ones, since the package states it will not wrap those, and the escape hatch is Unwrap rather than full support.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 21 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Wrapping http.ResponseWriter hides interfaces you did not know were there
Capturing a status code, a response time and a byte count from a Go handler looks trivial and is not, and httpsnoop exists to explain why. A http.ResponseWriter frequently implements more than http.ResponseWriter. The list the project names is http.Flusher, http.CloseNotifier, http.Hijacker, http.Pusher and io.ReaderFrom. Wrap the writer in your own struct that implements only the base interface and every one of those disappears, and code that was streaming a response, hijacking a connection or serving a file efficiently starts behaving differently. The failure is quiet, which is what makes it dangerous. A handler that checks whether the writer supports flushing will simply take the other branch. The project is blunt about the state of the alternatives it found, noting that advice suggesting this is a trivial undertaking has a high chance of breaking the application.
The wrapped writer implements exactly the same set of interfaces
The fix is detection rather than guessing. httpsnoop checks which of the additional interfaces the original writer implements, then returns a wrapped version that implements the same set and no more. If the underlying writer flushes, the wrapper flushes. If it does not, the wrapper does not pretend to. That is a different design from the other approach the project describes, where a struct implements all of the optional interfaces regardless: faking an interface the underlying writer does not support is hard to do correctly, and it is dangerous because an application may choose to behave differently purely on the presence of an interface. The repository layout shows how the wrapping is built. A codegen/ directory and a wrap_generated.go file sit next to the hand written capture_metrics.go, and the Makefile has a generate target running go generate, so the set of interface combinations is produced from a source of truth rather than typed out by hand.
CaptureMetrics gives you code, duration and bytes written
The high level API is one call. The README gives this example, where myH stands for your own handler:
// myH is your app's http handler, perhaps a http.ServeMux or similar.
var myH http.Handler
// wrappedH wraps myH in order to log every request.
wrappedH := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m := httpsnoop.CaptureMetrics(myH, w, r)
log.Printf(
"%s %s (code=%d dt=%s written=%d)",
r.Method,
r.URL,
m.Code,
m.Duration,
m.Written,
)
})
http.ListenAndServe(":8080", wrappedH)The returned metrics struct carries the status code, the duration of the request and the number of bytes written, which is everything a log line or a counter needs. The wrapper is itself an http.Handler, so it drops into an existing chain of middleware without changing the call sites below it. The project documentation also says the lower level API is exposed for users who want to control the wrapping themselves, which is the route to take when you want the measurements but not the canned struct.
WriteHeader twice, and writes that arrive after the handler returns
Beyond interface preservation, the package handles the awkward sequences that a hand written wrapper usually gets wrong. The documentation lists them explicitly: a WriteHeader that is never called, a WriteHeader that is called more than once, concurrent calls to the http.ResponseWriter methods, and calls that happen after the wrapped ServeHTTP has already returned. Each of these is a place where a naive wrapper can panic, double count or block. The last case is the one that catches people out, because a goroutine that outlives the request can still write to the writer, and the metrics have to stay consistent while that happens. The Makefile shows how this is checked, since the ci target runs go test -race -v ./... after regenerating the code, so the concurrency behaviour is exercised by the race detector rather than asserted in prose.
Unwrap is the escape hatch for interfaces httpsnoop cannot fake
The package is candid about the edges of its own coverage, and that candour is part of why it is usable. It may still be missing some interfaces provided by the Go core, and the author invites reports when one turns up. More limiting is the case of an application that adds its own interfaces to the mix: httpsnoop will not wrap those, because it can only reproduce the set it knows about. The documented way out is httpsnoop.Unwrap(w), which returns the underlying http.ResponseWriter so you can type-assert the result to whatever else you need. In practice that means a handler that depends on a bespoke interface has to unwrap first, which puts the responsibility back on your code, and that is the boundary to check before you put the package in front of a middleware chain that already wraps writers.
Rolling your own wrapper is the alternative, and it is the one the project argues against
There are two realistic ways to get these metrics, and the project describes both before arguing for its own. The first is to define your own struct that implements http.ResponseWriter and wrap with that, which is simple to write and quietly removes every optional interface the original writer supported, so streaming, hijacking and efficient file serving all change behaviour. The second is to return a struct that implements the full set of optional interfaces regardless of what the original supports, which preserves the surface area but fakes capabilities that may not exist underneath, and it changes application behaviour simply because an interface is now present. httpsnoop takes the third path, checking the original and mirroring it exactly, and its own summary is that it may still break your application but tries to avoid it as much as possible. The cost of that check is a small allocation and a type switch per request.
The benchmark in the README argues from a wide margin of error
The performance section publishes two numbers and then reasons about what they mean:
BenchmarkBaseline-8 20000 94912 ns/op
BenchmarkCaptureMetrics-8 20000 95461 ns/opThe author reads the difference as an overhead of roughly 500 ns per request on their machine, then immediately notes that the margin of error appears larger than the difference itself, so the conclusion drawn is that the overhead introduced by CaptureMetrics is negligible rather than a specific figure to bank on. That is an unusually careful framing for a README, and it is worth copying the caution along with the number, since the figures come from one machine and a different handler would produce different results. The scale of the underlying benchmark, around 95 microseconds per operation, also tells you that the measurement is dominated by the handler work rather than by the wrapper.
One module, no dependencies, and a recent release
The packaging is about as small as it gets. The go.mod file contains a module line for github.com/felixge/httpsnoop and a single go directive, go 1.24, with no require block, so the package pulls in nothing from elsewhere and the only decision is whether your build toolchain is new enough. The README does not include an installation command, so the path to the package is the import path itself and the Go Reference badge that links to the pkg.go.dev page for github.com/felixge/httpsnoop. On maintenance, the last push to the repository was on 2026-10-02, and the recent release history is thin rather than dead: v1.1.0 on 2026-06-11, then v1.0.4 from 2023-11-04 and v1.0.3 from 2022-05-08, so roughly two and a half years passed between the two most recent versions. The licence is MIT, which means no copyleft obligation for a service that depends on it and no per-use fee.
Editorial conclusion
httpsnoop suits a Go service that needs per-request metrics, latency logs or status code accounting and does not want to hand-roll a ResponseWriter wrapper, because it reproduces the exact optional interface set of the writer it replaces. It does not suit a codebase whose handlers depend on custom interfaces added on top of the standard ones, since the package states it will not wrap those, and the escape hatch is Unwrap rather than full support. Before adopting it, check the interface list your own middleware and handler code actually relies on, and note that the last push to the repository was on 2026-10-02 with v1.1.0 released on 2026-06-11.
Frequently asked questions
How do I add httpsnoop to a Go project?
The package is imported as github.com/felixge/httpsnoop, and the project's Go Reference badge links to the pkg.go.dev page for that import path. The repository's go.mod declares the module github.com/felixge/httpsnoop and go 1.24, with no other dependencies, and the README gives no install command beyond the import.
How do I capture the status code, duration and bytes written of a request?
Wrap your handler and call httpsnoop.CaptureMetrics(myH, w, r), which returns a metrics value with the Code, Duration and Written fields. The wrapper is itself an http.Handler, so it fits into an existing middleware chain.
Does wrapping the ResponseWriter break http.Flusher or http.Hijacker?
The package checks which additional interfaces the original http.ResponseWriter implements and returns a wrapped version implementing the exact same set, so a writer that supports flushing or hijacking keeps it. The documentation does note that some core interfaces may still be missing.
How do I reach the original ResponseWriter if I need a custom interface?
Call httpsnoop.Unwrap(w) to get the underlying http.ResponseWriter and type-assert it to the other interfaces you need. The package states it will not wrap interfaces your application adds on its own, so Unwrap is the documented route for that case.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/felixge-httpsnoop)Was this useful?