20.9. Prometheus
Table of Contents
Why Prometheus Matters for Backend Developers
Prometheus is one of the most popular tools for collecting and querying metrics from backend systems. It is used to answer questions like:
- How many requests per second is my API handling?
- What is the average response time?
- How much memory or CPU is my service using?
- Did error rates spike after the last deployment?
Prometheus is focused on metrics, not logs or traces. Metrics are numeric measurements over time, such as $requests\_total$ or $cpu\_usage\_percent$.
Key idea: Prometheus is a pull-based, time-series metrics database. It regularly scrapes (pulls) metrics from your services, stores them, and lets you query them with a special language called PromQL.
Compared to traditional monitoring tools, Prometheus is:
- Simple to run as a single binary.
- Great for containerized and cloud-native environments.
- Very efficient for time-series data and alerting.
In this chapter, you will learn what Prometheus is, how it works, which metrics you should export from backends, and how to query them.
Core Concepts of Prometheus
Time Series and Metrics
Prometheus stores time series: a sequence of values for the same metric over time.
Each time series is identified by:
- Metric name, for example:
http_requests_totalprocess_cpu_seconds_totalapi_response_time_seconds- A set of labels, which are key-value pairs that describe dimensions of the metric, such as:
method="GET"status="200"endpoint="/users"instance="api-1"
Together, these define one time series, for example:
http_requests_total{method="GET", status="200", endpoint="/users"}
Every time Prometheus scrapes your service, it gets the current value for each time series and attaches a timestamp.
Metric Types
Prometheus has 4 main metric types:
| Type | Description | Typical usage example |
|---|---|---|
| Counter | Value that only increases | Total HTTP requests, total errors |
| Gauge | Value that can go up and down | Memory usage, current active users |
| Histogram | Buckets that count observations by size | Request duration, response size distribution |
| Summary | Similar to histogram, client-side quantiles | Request duration with percentiles |
Rule:
- Use counters for things that are counted over time and only increase, like
*_total. - Use gauges for values that can go up and down, like memory or queue length.
- Use histograms or summaries for durations and sizes if you care about distribution (p50, p90, p99).
Example metric names:
http_requests_total(counter)http_in_progress_requests(gauge)http_request_duration_seconds(histogram)http_response_size_bytes(summary or histogram)
Pull-based Scraping
Prometheus does not wait for applications to push metrics. Instead, it pulls metrics from them.
Each instrumented service exposes an HTTP endpoint, usually /metrics. Prometheus periodically sends an HTTP GET request to that endpoint and parses the metrics.
Example request from Prometheus to your service:
GET http://api-service:8000/metrics
Your service responds with text like:
# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",status="200"} 1523
http_requests_total{method="GET",status="404"} 12
http_requests_total{method="POST",status="201"} 231Prometheus:
- Reads this text.
- Updates the internal time series.
- Stores them in its time series database.
This pull model has benefits:
- Prometheus controls how often to scrape.
- If a service is down, Prometheus automatically sees it as
up == 0. - No client needs to know where Prometheus is.
If you cannot expose a /metrics endpoint, you can use the Pushgateway, but that is a special case and less common for backend web services.
Targets and Jobs
Prometheus scrapes targets. A target is a single endpoint that exposes metrics, such as:
http://api-1:8000/metricshttp://api-2:8000/metricshttp://worker-1:9000/metrics
Targets are grouped into jobs. A job is a group of related targets that logically belong together.
Example jobs:
job="api"for all API instances.job="worker"for all background workers.job="postgresql"for database metrics exporter.
Prometheus automatically adds some labels:
job: the job name.instance: the specific target, likeapi-1:8000.
Example time series:
up{job="api", instance="api-1:8000"} 1up{job="api", instance="api-2:8000"} 0
up is a built-in gauge that is 1 when the target is reachable and 0 when it is not.
Prometheus Data Model and Labels
Labels and High Cardinality
Labels are powerful because they let you slice and filter metrics. For example:
- By HTTP method:
method="GET" - By status code:
status="500" - By endpoint:
endpoint="/users/{id}"(often normalized) - By region:
region="eu-west-1"
You can use labels to answer questions like:
- Error rate for POST requests to
/ordersin production. - Latency for a specific API instance.
However, you must be careful about high cardinality.
Cardinality is the number of unique label combinations.
Rule: Avoid labels that can take many different values, such as:
- User IDs
- Request IDs
- Full URLs with path parameters
- Random tokens or hashes
High cardinality explodes the number of time series and can make Prometheus slow or even unusable.
Good labels:
status="200",status="404",status="500"(limited set)method="GET",method="POST"(few values)service="api",service="worker"
Bad labels:
user_id="123456"session_id="ABCDEF"email="user@example.com"
Instead of labeling by user_id, you would use aggregate metrics like:
- Requests per endpoint.
- Errors per service.
- Latency per operation type.
Examples of Metric Naming and Labeling
Metric names should be:
- Lowercase.
- Words separated by underscores.
- With unit suffix where relevant, such as
_seconds,_bytes,_total.
| Good name | Meaning |
|---|---|
http_requests_total | Total HTTP requests |
http_request_duration_seconds | Duration of HTTP requests in seconds |
queue_size | Current size of some queue |
db_queries_total | Number of database queries |
db_query_duration_seconds | Duration of database queries in seconds |
Example for an API:
http_requests_total{method="GET",status="200",endpoint="/users"} 1523
http_requests_total{method="GET",status="404",endpoint="/users"} 12
http_requests_total{method="POST",status="201",endpoint="/users"} 231This allows you to ask:
- All requests:
sum(http_requests_total) - Only errors:
sum(http_requests_total{status!="200"}) - Only GET requests to
/users:sum(http_requests_total{method="GET",endpoint="/users"})
Prometheus in Practice for Backend Services
Typical Setup Overview
A common basic monitoring setup for a backend application looks like this:
- Your backend application (for example FastAPI) exposes:
/metricsfor Prometheus./healthfor health checks.- Prometheus is configured to scrape:
- All API instances.
- Background workers.
- Databases through exporters.
- Redis through exporters.
- Grafana is connected to Prometheus for visual dashboards.
High level flow:
- Your app exports metrics at
/metrics. - Prometheus scrapes
/metricsevery few seconds. - Prometheus stores the data.
- Grafana visualizes metrics and dashboards.
- Alertmanager (an extra component) sends alerts based on rules.
Exporting Metrics from a Web Backend
Your job as a backend developer is usually to:
- Instrument your code with metrics.
- Expose a
/metricsendpoint. - Make sure Prometheus can reach that endpoint.
In Python, a typical approach is:
- Use the
prometheus_clientlibrary to define metrics:
from prometheus_client import Counter, Histogram
REQUESTS_TOTAL = Counter(
"http_requests_total",
"Total HTTP requests",
["method", "endpoint", "status"],
)
REQUEST_DURATION_SECONDS = Histogram(
"http_request_duration_seconds",
"HTTP request duration in seconds",
["method", "endpoint", "status"],
)- Update them in your request handling code (for example in FastAPI middleware):
import time
from fastapi import FastAPI, Request
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
from starlette.responses import Response
app = FastAPI()
@app.middleware("http")
async def metrics_middleware(request: Request, call_next):
method = request.method
endpoint = request.url.path
start = time.perf_counter()
try:
response = await call_next(request)
status = str(response.status_code)
except Exception:
status = "500"
raise
finally:
duration = time.perf_counter() - start
REQUESTS_TOTAL.labels(method=method, endpoint=endpoint, status=status).inc()
REQUEST_DURATION_SECONDS.labels(
method=method, endpoint=endpoint, status=status
).observe(duration)
return response
@app.get("/metrics")
def metrics():
data = generate_latest()
return Response(data, media_type=CONTENT_TYPE_LATEST)- Prometheus scrapes
http://your-api:8000/metrics.
Example content of /metrics in text form:
# HELP http_requests_total Total HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",endpoint="/users",status="200"} 1523
http_requests_total{method="GET",endpoint="/users",status="404"} 12
http_requests_total{method="POST",endpoint="/users",status="201"} 231
# HELP http_request_duration_seconds HTTP request duration in seconds
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.1",method="GET",endpoint="/users",status="200"} 1300
http_request_duration_seconds_bucket{le="0.5",method="GET",endpoint="/users",status="200"} 1490
http_request_duration_seconds_bucket{le="1",method="GET",endpoint="/users",status="200"} 1510
http_request_duration_seconds_bucket{le="+Inf",method="GET",endpoint="/users",status="200"} 1523
http_request_duration_seconds_sum{method="GET",endpoint="/users",status="200"} 48.2
http_request_duration_seconds_count{method="GET",endpoint="/users",status="200"} 1523You do not have to write these buckets yourself. The client library generates them.
Prometheus Configuration for Your Service
Prometheus uses a YAML configuration file, typically prometheus.yml. A very simple configuration that scrapes one backend app might look like this:
global:
scrape_interval: 15s # how often to scrape by default
scrape_configs:
- job_name: "api"
scrape_interval: 5s # override for this job
static_configs:
- targets:
- "api-1:8000"
- "api-2:8000"
This directs Prometheus to scrape the /metrics endpoint from both instances every 5 seconds.
If you use Docker Compose, this might look like:
services:
api:
image: my-api-image
ports:
- "8000:8000"
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
Prometheus will then be accessible at http://localhost:9090.
PromQL: Querying Metrics
Prometheus comes with its own query language called PromQL (Prometheus Query Language). You use it to:
- Inspect metrics in the Prometheus UI.
- Create graphs in Grafana.
- Define alerting rules.
Basic PromQL Examples
Selecting a Metric
Return all time series for http_requests_total:
http_requests_totalFilter by label, for example only GET requests:
http_requests_total{method="GET"}Filter out 2xx responses:
http_requests_total{status!~"2.."}Rates and Counters
Counters only increase, so you are usually interested in the rate of increase.
PromQL has functions like rate and irate.
rate(metric[5m])= average per second rate over the last 5 minutes.irate(metric[5m])= instant per second rate using the last 2 data points in that 5-minute range.
Example: requests per second over the last 5 minutes:
rate(http_requests_total[5m])Sum across all instances:
sum(rate(http_requests_total[5m]))Requests per second per status code:
sum by (status) (rate(http_requests_total[5m]))sum(rate(http_requests_total{status!~"2.."}[5m]))
/
sum(rate(http_requests_total[5m]))
Formula:
To compute error rate, define:
$$
\text{error\_rate} = \frac{\text{error\_requests\_per\_second}}{\text{total\_requests\_per\_second}}
$$
In PromQL:
Gauges and Averages
For gauges, you use functions like avg, max, min.
Example: average memory usage across instances:
avg(process_resident_memory_bytes)Maximum CPU usage per instance:
max by (instance) (rate(process_cpu_seconds_total[5m]))Histograms and Latency
Histograms store values in buckets. For request duration, you can estimate percentiles using histogram_quantile.
Assume you have:
http_request_duration_seconds_buckethttp_request_duration_seconds_sumhttp_request_duration_seconds_count
To compute p90 latency for GET requests to /users:
histogram_quantile(
0.90,
sum by (le) (
rate(http_request_duration_seconds_bucket{method="GET", endpoint="/users"}[5m])
)
)This returns the 90th percentile in seconds.
Similarly, p99 for all requests:
histogram_quantile(
0.99,
sum by (le) (
rate(http_request_duration_seconds_bucket[5m])
)
)These queries are commonly used in Grafana dashboards.
Examples of Useful Backend Queries
Here are some queries that are typical for monitoring a REST API.
Total Requests per Second
sum(rate(http_requests_total[5m]))Requests per second by method:
sum by (method) (rate(http_requests_total[5m]))Error Rate
5xx error requests per second:
sum(rate(http_requests_total{status=~"5.."}[5m]))Percentage of non-2xx requests:
sum(rate(http_requests_total{status!~"2.."}[5m]))
/
sum(rate(http_requests_total[5m]))
* 100Latency
p95 latency for all requests:
histogram_quantile(
0.95,
sum by (le) (rate(http_request_duration_seconds_bucket[5m]))
)
p95 latency for /orders endpoint:
histogram_quantile(
0.95,
sum by (le) (rate(http_request_duration_seconds_bucket{endpoint="/orders"}[5m]))
)Instance Health
Check if all instances are up:
upOnly API instances:
up{job="api"}Instances that are down:
up == 0Example: Full Flow from Code to Graph
To connect the whole picture, consider this small scenario.
1. Instrumentation in the API
You add metrics in your API using a library, for example in Python:
from prometheus_client import Counter
ERRORS_TOTAL = Counter(
"http_errors_total",
"Total HTTP errors",
["method", "endpoint", "status"],
)
# inside your error handler or middleware:
ERRORS_TOTAL.labels(method=method, endpoint=endpoint, status=status).inc()2. Metrics Endpoint
You expose a /metrics endpoint that returns all metrics (including your custom ones).
3. Prometheus Scrape Configuration
In prometheus.yml:
scrape_configs:
- job_name: "api"
scrape_interval: 5s
static_configs:
- targets:
- "api-1:8000"
- "api-2:8000"4. Prometheus Stores Data
Every 5 seconds, Prometheus pulls metrics and keeps appending the newest values to the time series for http_errors_total.
5. Grafana Dashboard
In Grafana, you create a panel with the query:
sum(rate(http_errors_total[5m]))This shows you how many errors per second happen, averaged over the last 5 minutes.
You can also break it down by endpoint:
sum by (endpoint) (rate(http_errors_total[5m]))Now you can see which endpoint is most problematic.
Integration with Other Components
Prometheus rarely runs alone in a production backend. It is usually part of a monitoring stack:
| Component | Purpose |
|---|---|
| Prometheus | Scrapes metrics, stores time series |
| Alertmanager | Sends alerts based on Prometheus rules |
| Grafana | Visualizes metrics and creates dashboards |
| Exporters | Expose metrics from systems like databases, Redis, OS |
Examples of exporters you might use:
- Node exporter for Linux host metrics (CPU, disk, memory).
- PostgreSQL exporter for database metrics.
- Redis exporter for Redis metrics.
- Blackbox exporter for HTTP endpoint checks.
Your backend services just become additional targets in this ecosystem.
Practical Tips and Common Pitfalls
Tips for Designing Metrics
- Start with a few key metrics:
- Requests per second.
- Error rate.
- Latency (p50, p90, p99).
- Database query rate and latency.
- Queue size for background jobs.
- Use consistent naming and labeling.
- Include a small number of well-chosen labels, such as:
methodendpoint(normalized)statusservice
Avoiding Common Problems
- Do not label metrics with unbounded values like user IDs.
- Do not create too many different metric names; prefer labels to separate dimensions.
- Be careful with very frequent scraping if you have many targets.
- Watch disk space, since Prometheus stores all time series.
A rough rule: if you have:
- N targets,
- M metrics per target,
- K label combinations per metric,
Then total time series β $N \times M \times K$.
Keep this number reasonable or Prometheus may struggle.
Rule of thumb: Try to keep time series count manageable. Many production setups stay under a few million time series. As a beginner, if you are in the tens of thousands, you are already fine and should avoid exploding it with high-cardinality labels.
Summary
In this chapter you learned:
- Prometheus is a time-series database and monitoring system focused on metrics.
- It scrapes targets that expose a
/metricsendpoint, rather than waiting for metrics to be pushed. - Metrics are time series identified by a name and a set of labels.
- You can instrument your backend to expose counters, gauges, histograms, and summaries.
- PromQL lets you query metrics and calculate rates, errors, and latency percentiles.
- Prometheus is usually used together with Grafana and Alertmanager as part of an observability stack.
For a backend developer, Prometheus is a key tool to understand how your application behaves in production, detect problems early, and verify that changes did not break performance or reliability.
Views: 7
KAHIBARO