A metric named request_duration leaves readers guessing whether 250 means seconds, milliseconds, or something else. A Prometheus-facing name such as http_request_duration_seconds carries that contract into queries, recording rules, and alerts. The contract still needs a correct implementation. Appending _seconds to measurements collected in milliseconds changes the spelling, not the magnitude, and can create a thousandfold monitoring error.
Convert at a defined boundary
Choose one quantity and one unit for each metric family. Prometheus recommends base units such as seconds and bytes, with a unit suffix where applicable. A duration and an absolute timestamp can both use seconds, but they are different quantities: last_success_timestamp_seconds should not be combined with request_duration_seconds. Describe the measurement and boundary in its help text so that a useful name has an equally clear definition.
If a library reports milliseconds, convert once before observation or export. Preserve sufficient precision instead of truncating a short duration to zero. Configure the dashboard to display the base unit appropriately; a formatter may show 0.250 seconds as 250 milliseconds without changing stored samples. Do not add another conversion in a recording rule because the panel happens to use milliseconds on its axis.
Unit and type answer different questions
An accumulating byte count conventionally ends in _bytes_total; a current memory usage gauge ends in _bytes. Both measure bytes, but only the former represents accumulated work and is suitable for a counter rate. Counts of requests conventionally use _total without inventing a physical unit. Histogram families add their own component suffixes, such as _seconds_bucket, _seconds_sum, and _seconds_count, to the duration family name.
A ratio commonly represents a fraction between zero and one; percentages use a different numeric scale. State which one a panel expects. A counter may reset when its process restarts, and Prometheus counter-rate functions account for decreases within the observed series. An additional _created series is not required for that behavior. Apply rate calculations before aggregating independent counters so that their individual resets remain visible.
When it breaks
A correctly named metric can still contain incorrect values. Mixing seconds and milliseconds under the same identity contaminates history, so changing only a label or help string cannot repair old samples. Treat a unit migration as a contract change: update the producer, queries, alerts, and dashboards together, and keep historical comparisons explicit. A new identity may be appropriate when the old series has an incompatible meaning.
The suffix also cannot enforce counter behavior. A gauge named _total remains a bad input to a counter-rate interpretation if it moves down during ordinary operation. Conversely, dividing an already calculated per-second rate by the interval again invents a different quantity. These are dimensional and semantic errors, not display problems. Other telemetry conventions may carry units in metadata rather than names; follow the documented exporter mapping instead of renaming every upstream instrument blindly.
Check it quickly
Save the following as units.py and run Python 3. It converts three synthetic latency measurements and emits a small Prometheus text exposition with help and type declarations. Decimal arithmetic makes the millisecond-to-second conversion explicit. The comparison demonstrates why a renamed but unconverted producer cannot share the same latency threshold.
from decimal import Decimal
latency_ms = [Decimal('120'), Decimal('250'), Decimal('630')]
latency_seconds = [value / Decimal('1000') for value in latency_ms]
threshold_seconds = Decimal('0.300')
correct_good = sum(value <= threshold_seconds for value in latency_seconds)
rename_only_good = sum(value <= threshold_seconds for value in latency_ms)
transferred_bytes = 1024 * 1024
used_ratio = Decimal('25') / Decimal('100')
assert correct_good == 2 and rename_only_good == 0
assert sum(latency_seconds) == Decimal('1.000')
assert used_ratio == Decimal('0.25')
print(f"# within 0.300s: converted={correct_good}/3, rename_only={rename_only_good}/3")
print('# HELP demo_last_request_duration_seconds Duration of the most recent completed request.')
print('# TYPE demo_last_request_duration_seconds gauge')
print(f'demo_last_request_duration_seconds {latency_seconds[-1]:.3f}')
print('# HELP demo_transferred_bytes_total Bytes transferred since this process started.')
print('# TYPE demo_transferred_bytes_total counter')
print(f'demo_transferred_bytes_total {transferred_bytes}')
print('# HELP demo_capacity_used_ratio Fraction of configured capacity currently used.')
print('# TYPE demo_capacity_used_ratio gauge')
print(f'demo_capacity_used_ratio {used_ratio}')
The first comment reports two of three observations within 0.300 seconds after conversion, compared with zero of three when millisecond values are merely treated as seconds. The exported last-request gauge is 0.630 seconds, the byte counter is 1,048,576, and the capacity ratio is 0.25, corresponding to 25% in a percentage display.
The complete output passed promtool check metrics using Prometheus 3.5.1. That parser check establishes valid exposition and naming lint for this sample; it cannot infer whether real instrumentation measured the promised quantity. Verify a known input through collection, export, query, and display before trusting a renamed production metric.
Sources
- Prometheus: Metric and Label Naming, base units, suffixes, and conventions at the Prometheus boundary, checked 2026-09-13.
- Prometheus: Data Model, metric identity and labels, checked 2026-09-13.
- Prometheus: Query Functions, counter rates and reset handling, checked 2026-09-13.