Skip to content
ExponaBLOG
PROMETHEUS, GRAFANA & MONITORING · DAY 081 / 100

Monitoring APIs by route: a hands-on guide for engineers

Find which endpoint is slow.

Expona Admin5 min read

The challenge of finding slow endpoints

When your API is misbehaving at 2 AM, you need to know which endpoint is actually causing the problem—not just that "something is slow." Generic latency metrics tell you the symptom, but monitoring APIs by route is a hands-on guide for engineers who need to pinpoint the exact culprit. By breaking down response times, error rates, and throughput by individual routes and HTTP methods, you transform vague alerts into actionable intelligence.

Label by route template and method

The foundation of route-level monitoring is capturing two critical dimensions: the route template and the HTTP method. Without these labels, you end up averaging latencies across completely different operations—a fast GET and a slow POST on the same endpoint blur together into misleading averages.

Most frameworks let you extract the route pattern before incrementing your Prometheus counter. In Express with a custom middleware, you'd capture the matched route path:

javascript
app.use((req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    const route = req.route ? req.route.path : 'unknown';
    const method = req.method;
    
    httpRequestDuration.labels(method, route, res.statusCode).observe(duration / 1000);
  });
  
  next();
});

In .NET with Prometheus-net, you'd register middleware that captures these labels:

csharp
app.UseHttpMetrics(options =>
{
    options.ReduceStatusCodeCardinality();
});

app.MapPrometheusScrapingEndpoint();

The key is using route templates (like /users/{id}) rather than literal paths (like /users/12345). If you don't, cardinality explodes—you'll create a new time series for every unique user ID, exhausting your Prometheus storage.

Combine method and route into your label structure. A histogram or counter should look like this in PromQL:

yaml
http_request_duration_seconds_bucket{method="POST",route="/orders",le="0.5"}
http_request_duration_seconds_bucket{method="GET",route="/orders",le="0.5"}

This separation lets you query latency for POST requests to your orders endpoint independently from GET requests, revealing that your create operation is the real bottleneck.

Separate status classes

Not all requests are created equal. A 500 error might return in 50 ms because the database connection failed immediately, while a successful 200 takes 500 ms because it ran complex business logic. Lumping them together distorts your understanding of whether your system is actually healthy.

Add status code (or status class) as a label. Most teams prefer grouping: 2xx for success, 4xx for client errors, 5xx for server errors. This keeps cardinality reasonable while still letting you detect when error rates spike on specific routes:

yaml
http_requests_total{method="POST",route="/payments",status_class="5xx"} 45
http_requests_total{method="POST",route="/payments",status_class="2xx"} 9998

Create separate alerts for error rates by route. A sudden spike in 5xx responses on your payment endpoint demands immediate attention; a spike in 4xx might just mean clients are sending malformed requests.

In Grafana, build dashboards that show both success and error latency side by side. You might discover that your /search endpoint's 95th percentile latency is driven almost entirely by failed queries—the successful ones are actually fast.

Top routes by latency

Finding your slowest endpoints should be a regular part of your routine. Use a Grafana table panel with a PromQL query that ranks routes by their p95 or p99 latency:

yaml
histogram_quantile(0.95, 
  sum by (method, route) (
    rate(http_request_duration_seconds_bucket[5m])
  )
) > 0

Sort descending and look for the usual suspects. Often you'll find routes you didn't know existed are taking hundreds of milliseconds. A forgotten reporting endpoint that joins three tables could be dominating your resource usage.

Create a dashboard panel showing the top 10 routes by latency, updated every minute. Pair it with another panel showing the top 10 by request volume. Routes can be slow for two reasons: they're inherently slow, or they're simply being called a lot. Both need attention, but the solutions differ.

Set up alerts based on route-specific SLOs. Your /checkout endpoint might have a 500 ms target; your /analytics endpoint might tolerate 5 seconds. Generic alerts miss this nuance:

yaml
alert: SlowCheckoutEndpoint
expr: histogram_quantile(0.95, sum by (route) (rate(http_request_duration_seconds_bucket{route="/checkout"}[5m]))) > 0.5
for: 5m

This way you catch degradation on critical paths before users notice, while ignoring expected slowness on non-critical routes.

Avoid per-user labels

A tempting mistake is adding user ID as a label to understand which user is experiencing problems. Don't. User cardinality is unbounded—in a system with millions of users, this creates millions of time series that will crash your Prometheus instance.

The same goes for request IDs, session tokens, or any other unique identifier. These belong in application logs, not in metrics.

If you need to correlate slow requests with specific users, use distributed tracing. Send trace IDs to your logs and metrics, then use Loki or your log aggregator to drill down:

  1. Grafana shows route /search has high latency.
  2. You open Loki and filter for that route during the time window.
  3. You see specific trace IDs associated with slow queries and follow them to the user.

This approach keeps metrics cardinality under control while still giving you the investigative power you need. The metrics answer "which endpoint," traces answer "why for this specific request."

Common mistakes

  • Using literal paths instead of templates. Every unique ID creates a new time series. Use your framework's route matching to extract the template path before recording metrics.
  • Forgetting status codes. Successful and failed requests have very different latency profiles. Always include status or status class as a label.
  • Over-aggregating. Don't merge POST and GET latencies for the same route. They're fundamentally different operations and need separate visibility.
  • Adding unbounded cardinality dimensions. User IDs, session tokens, and request IDs belong in logs and traces, not metric labels.
  • Not setting route-specific SLOs. A generic "all endpoints should be under 1 second" alert produces noise and misses real problems on critical paths.
  • Ignoring volume.. A route with 10 requests per day is not worth optimizing even if it's technically slow. Context matters—watch volume alongside latency.

Key takeaways

  • Label by route template and HTTP method to understand which specific operation is slow, not just that something is slow.
  • Separate status codes or status classes (2xx, 4xx, 5xx) so errors don't hide the true latency of successful operations.
  • Build a "top routes by latency" dashboard and make it part of your regular incident response workflow.
  • Never add unbounded cardinality labels like user ID or request ID to metrics; use distributed traces and logs for that level of detail instead.
  • Set route-specific SLOs and alerts; one size does not fit all for your API surface.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles