The Prometheus client exposes metrics in a format collected by a Prometheus server. A useful metric answers an operational question, such as error rate or operation duration, without carrying personal data.
Prometheus normally uses a pull model: at configured intervals it requests an HTTP endpoint and records one sample for every series. A metric name plus its complete label set identifies a series. Changing any label value creates another series. That property enables expressive queries, but it is also why cardinality must be designed before instrumentation ships.
from prometheus_client import Counter, Histogram
REQUESTS = Counter(
"app_requests_total", "Total requests", ["method", "result"]
)
DURATION = Histogram(
"app_request_duration_seconds", "Request duration", ["route"]
)
def process() -> None:
with DURATION.labels(route="/orders").time():
run_work()
REQUESTS.labels(method="POST", result="success").inc()
Counters record accumulated events, Gauges represent current values, and Histograms place observations in buckets. Choose buckets that match realistic latency targets.
A Counter represents events that accumulate, such as requests or failures. The client exposes the _total suffix where appropriate. Do not use one for a value that decreases. A Gauge fits active jobs, temperature, or current queue depth, although aggregation across replicas can be misleading unless the meaning is explicit.
A Histogram keeps cumulative counts by boundary, plus the sum and count of observations. PromQL can calculate quantiles and aggregate instances from those series. A Summary calculates statistics in the client; quantiles from different instances cannot be combined in the same way. Histograms are usually more flexible for distributed request latency.
Design the metric from the question
Begin with the operational question and the query that will answer it. An HTTP error rate needs a counter grouped by a small result or status family. Latency needs a histogram with boundaries around service objectives. Queue backlog can use a gauge for the current value, while counters for arrivals and completions explain movement.
Names should include an unambiguous unit and type:
_secondsfor duration in seconds;_bytesfor size;_totalfor counters;- an application or subsystem prefix to avoid collisions.
Do not encode labels into names such as orders_post_success_total when a small, useful dimension models them better. Conversely, do not add labels that will never appear in a query or alert. Every dimension multiplies possible series.
Choose buckets around an objective
Default buckets do not fit every service. If an endpoint should respond within 300 ms, include boundaries around that target and cover the expected tail:
from prometheus_client import Histogram
LATENCY = Histogram(
"checkout_duration_seconds",
"Checkout duration",
["result"],
buckets=(0.05, 0.1, 0.2, 0.3, 0.5, 1.0, 2.0, 5.0),
)
Too many buckets multiply series; too few hide the distribution. Inspect real measurements and adjust through a planned change. Changing boundaries disrupts the logical continuity of the affected series, so coordinate dashboards and rules.
To measure the fraction below 300 ms, divide the rate of the le="0.3" bucket by the rate of _count. For approximate percentiles, histogram_quantile operates on bucket rates. Its resolution comes from the chosen boundaries because the server interpolates within a bucket rather than retaining individual values.
Expose the endpoint safely
The package provides start_http_server for simple scripts and workers:
from prometheus_client import start_http_server
start_http_server(8000, addr="127.0.0.1")
Web applications should use the integration that matches their WSGI or ASGI server and expose one /metrics endpoint. Do not start an extra listener in every worker without understanding multiprocess behavior. Restrict the listening interface and allow only the scraper or monitoring network. A proxy or service mesh can provide TLS and authentication when that matches the infrastructure.
Do not make the endpoint public for convenience. Even without explicit credentials, metrics reveal route names, dependencies, and traffic patterns. Never place secrets in labels or descriptions, and prevent raw request parameters from becoming metric names.
Account for processes and workers
In a single process, the default registry collects application and runtime metrics. A multiprocess server gives each worker independent memory. Scraping one worker or summing values naively produces incomplete or incorrect data. The client's multiprocess mode stores state in files under a shared directory and requires integration with the server lifecycle.
Read the integration documentation before enabling it: set the directory before importing the application, clear it at deployment startup, and mark dead processes according to the server hooks. Never share it between unrelated instances. Some metric types and features have limitations in this mode. If the architecture permits, individually scraped single-worker processes may be simpler.
Very short jobs disappear before Prometheus can scrape them. Pushgateway is intended for a restricted class of service-level batch jobs. It is not a general replacement for pull and should not receive a unique series for every run. Group by the stable job identity and let the gateway represent its latest meaningful result.
Bound labels and cardinality
Labels need a small predictable value set. Never use a full URL, error message, email, token, or user ID. Normalize routes such as /orders/{id}. Protect the metrics endpoint according to your network model because service names and traffic patterns are operational information.
Estimate the upper bound before release: methods × routes × results × replicas × buckets. Ten routes, four results, five replicas, and ten histogram buckets already produce hundreds of series for one instrument. User identifiers and free-form errors make that set effectively unbounded.
Initialize expected combinations when a missing series would confuse an alert. A labeled counter does not exist until it is observed. Pre-creating a tiny set such as result="success" and result="error" makes zero explicit. Do not pre-initialize large combinations.
Avoid repeating infrastructure labels that Prometheus already assigns to the target, such as host or environment, unless the design requires it. Static deployment dimensions generally belong to scrape configuration or service discovery rather than application code.
Test instrumentation behavior
Use an isolated CollectorRegistry so tests do not include process-global collectors:
from prometheus_client import CollectorRegistry, Counter, generate_latest
registry = CollectorRegistry()
events = Counter("test_events_total", "Events", registry=registry)
events.inc()
text = generate_latest(registry).decode("utf-8")
assert "test_events_total 1.0" in text
Test behavior rather than only checking that a name exists. Confirm success and failure increment exactly once, timers complete during exceptions, and label values belong to their allowed set. An endpoint test should also check its content type and access policy.
Review where instrumentation begins and ends. Incrementing before an operation completes may report success after an exception. Timing only an inner call may omit meaningful queueing. State the measured boundary in the help text and keep code aligned with it.
Combine metrics with OpenTelemetry traces and structured logs without copying every field into all signals.
The official Prometheus Python Client documentation and the official metric naming practices, accessed July 22, 2026, cover metric types, exporting, integrations, and conventions. Cardinality is the central design constraint: a few deliberate labels create a more sustainable system.