Skip to content

perf: collect metrics once per route instead of per handler - #7492

Merged
mholt merged 4 commits into
caddyserver:masterfrom
veeceey:fix/issue-4644-prometheus-metrics-perf
Mar 3, 2026
Merged

mholt merged 4 commits into
caddyserver:masterfrom
veeceey:fix/issue-4644-prometheus-metrics-perf

Conversation

@veeceey

@veeceey veeceey commented Feb 17, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #4644

The current metrics implementation wraps every individual middleware handler with metricsInstrumentedHandler. So if a route has 5 handlers, metrics get collected 5 separate times per request -- even though all handlers see the same request and the per-handler metrics end up being redundant (as the issue describes in detail).

This was profiled as consuming 73% of request handling CPU time for simple handlers, and @mholt measured a 12-15% overall throughput improvement by disabling the per-handler wrapping entirely.

What this changes

Instead of wrapping each handler individually in wrapMiddleware, metrics are now collected once per route match via a new metricsInstrumentedRoute type. This is applied in wrapRoute after the middleware chain is compiled, so the entire chain is instrumented as a single unit. The handler label uses the first handler's module name (the most meaningful identifier).

The old metricsInstrumentedHandler has been removed as it is no longer used and is unexported, so there is no backward compatibility concern.

Benchmark results

With 5 handlers per route:

BenchmarkMultipleHandlersWithMetrics-16     261052    4643 ns/op   4400 B/op   45 allocs/op
BenchmarkSingleRouteMetrics-16             1308663     919 ns/op    816 B/op    8 allocs/op

~5x faster, ~5.4x less memory, ~5.6x fewer allocations

Testing

  • All existing tests pass (the two pre-existing failures in TestRecursiveImport and TestACMEServerWithDefaults are unrelated and fail on master as well)
  • Added TestMetricsInstrumentedRoute for the new route-level wrapper
  • Added comparative benchmarks showing old vs new behavior

This is tagged help wanted and optimization on the issue. Happy to iterate on the approach if the maintainers have preferences on the handler label strategy or want to discuss further.

AI Disclosure

AI tools (Claude) were used to assist with drafting parts of this PR, including code generation and review. All changes have been manually reviewed and tested.

…ver#4644)

Move Prometheus metrics instrumentation from the per-handler level to
the per-route level. Previously, every middleware handler in a route was
individually wrapped with metricsInstrumentedHandler, causing metrics to
be collected N times per request (once per handler in the chain). Since
all handlers in a route see the same request, these per-handler metrics
were redundant and added significant CPU overhead (73% of request
handling time per the original profiling).

The fix introduces metricsInstrumentedRoute which wraps the entire
compiled handler chain once in wrapRoute, collecting metrics only when
the route actually matches. The handler label uses the first handler's
module name, which is the most meaningful identifier for the route.

Benchmark results (5 handlers per route):
  Old (per-handler):  ~4650 ns/op, 4400 B/op, 45 allocs/op
  New (per-route):    ~940 ns/op,  816 B/op,   8 allocs/op
  Improvement:        ~5x faster, ~5.4x less memory, ~5.6x fewer allocs

Signed-off-by: Varun Chawla <varun_6april@hotmail.com>
@CLAassistant

CLAassistant commented Feb 17, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@francislavoie

Copy link
Copy Markdown
Member

FYI @hairyhenderson, any objections? It was your decision originally to wrap handlers this way.

The old metricsInstrumentedHandler is kept for backward compatibility but is no longer used by the default provisioning path.

Please delete any unused code, I don't think there's any reason to keep it around if it's being replaced. It's not exported if it starts with a lowercase letter so there's no BC break.

@francislavoie francislavoie added the optimization 📉 Performance or cost improvements label Feb 17, 2026
@francislavoie francislavoie added this to the 2.x milestone Feb 17, 2026
Delete the metricsInstrumentedHandler type, its constructor, and
ServeHTTP method since they are no longer used after switching to
route-level metrics collection via metricsInstrumentedRoute. Also
remove the unused metrics parameter from wrapMiddleware and the
middlewareHandlerFunc test helper, and convert existing tests to
use the new route-level API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread modules/caddyhttp/metrics.go Outdated
Comment on lines +217 to +218
// taken from https://github.com/prometheus/client_golang/blob/6007b2b5cae01203111de55f753e76d8dac1f529/prometheus/promhttp/instrument_server.go#L298
func computeApproximateRequestSize(r *http.Request) int {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff is bigger than it should be. Move this function back to the bottom where it used to be.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, sorry about the unnecessary diff noise. I'll move the function back to its original position at the bottom of the file in the next push.

Comment on lines -235 to -239
// the "code" value is set later, but initialized here to eliminate the possibility
// of a panic
statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""}

// Determine if this is an HTTPS request

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the useful comments were dropped. Please keep them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for that — I'll restore the original comments in the next push. They provide important context that shouldn't have been removed.

@veeceey

veeceey commented Feb 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review feedback @francislavoie! I'll address all three items in the next push:

  1. Move the function back to the bottom of the file to minimize the diff
  2. Restore the original comments that were accidentally dropped
  3. Delete the old metricsInstrumentedHandler since it's unexported and no longer used

Will update shortly.

- Move computeApproximateRequestSize back to bottom of file to minimize diff
- Restore all useful comments that were accidentally dropped
- Old metricsInstrumentedHandler already removed in previous commit
@mholt

mholt commented Feb 18, 2026 •

Copy link
Copy Markdown
Member

We also need your AI disclosure, which you deleted from the PR template. You are definitely using AI and we require disclosure.

@mholt mholt closed this Feb 18, 2026
@mholt mholt reopened this Feb 18, 2026
@mholt

mholt commented Feb 18, 2026

Copy link
Copy Markdown
Member

Reopening since we probably do want something like this, but I want to emphasize how important an honest AI disclosure is.

@veeceey

veeceey commented Feb 20, 2026

Copy link
Copy Markdown
Contributor Author

Hey @mholt, sorry about that — you're absolutely right. I used Claude (Anthropic) to help with parts of this PR, including drafting code and commit messages. I'll update the PR description with a proper AI disclosure section right away. Won't happen again.

@veeceey

veeceey commented Feb 20, 2026

Copy link
Copy Markdown
Contributor Author

Updated the PR description with AI disclosure and addressed the remaining code feedback. Thanks for keeping things transparent!

@mholt mholt modified the milestones: 2.x, v2.11.1 Feb 20, 2026

@mholt mholt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@mholt
mholt merged commit dc36082 into caddyserver:master Mar 3, 2026
27 checks passed
@SuperQ

SuperQ commented Mar 4, 2026

Copy link
Copy Markdown

Interesting, I didn't realize that there were any allocs involved at all for these.

Standard Prometheus client_golang should be basically alloc-free for most use.

See this blog post and these test results. It seems like this is mostly the middleware wrapping and not the actual instrumentation that is the cause?

@github-actions github-actions Bot mentioned this pull request Mar 6, 2026
4 tasks
@francislavoie

Copy link
Copy Markdown
Member

You're gonna need to elaborate @Ember-ruby

@steadytao

Copy link
Copy Markdown
Member

sure! https://www.merriam-webster.com/dictionary/slop#dictionary-entry-1

If you think it should be reverted, please point to the concrete technical problem in the change itself, for example a correctness issue, a regression, a maintainability concern or a benchmark/result you believe is misleading. Otherwise there is not much for anyone to engage with.

@mholt

mholt commented Apr 16, 2026

Copy link
Copy Markdown
Member

You're wasting our time @Ember-ruby -- one more chance before a block so we can get back to work.

@veeceey

veeceey commented Apr 19, 2026 via email

Copy link
Copy Markdown
Contributor Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

optimization 📉 Performance or cost improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prometheus metrics are redundant and slow

6 participants