Measuring execution time looks easy, but small methodological mistakes can make a benchmark misleading. Python’s time module provides perf_counter_ns(), a high-resolution performance clock that returns an integer number of nanoseconds. It is useful for comparing short operations, recording latency, and avoiding floating-point precision loss during long measurement series. This guide explains how to use it correctly, interpret its output, and build more reliable performance measurements.
What perf_counter_ns returns
time.perf_counter_ns() returns the current value of a performance counter in nanoseconds. The absolute number is not a calendar timestamp. It only becomes meaningful when compared with another reading from the same counter. Record one value before an operation, another after it, and subtract.
from time import perf_counter_ns
start = perf_counter_ns()
result = sum(i * i for i in range(100_000))
end = perf_counter_ns()
print(f"Duration: {end - start} ns")
The counter includes elapsed time while the process waits, so it measures wall-clock latency as experienced by the application. Python selects the highest-resolution performance clock available on the platform.
Difference from perf_counter
perf_counter() returns seconds as a floating-point value. perf_counter_ns() returns nanoseconds as an integer. Both use the same underlying performance clock, but the nanosecond version avoids repeated conversion and floating-point rounding when durations are tiny or many samples are accumulated.
The name does not guarantee true one-nanosecond hardware resolution. Actual resolution depends on the operating system and clock source. The suffix only describes the unit and integer return type.
Converting to readable units
duration_ns = end - start
microseconds = duration_ns / 1_000
milliseconds = duration_ns / 1_000_000
seconds = duration_ns / 1_000_000_000
Keep the original integer for calculations and convert only when displaying or exporting results. This minimizes accumulated rounding errors.
A reusable timing helper
from time import perf_counter_ns
from collections.abc import Callable
from typing import TypeVar
T = TypeVar("T")
def measure(function: Callable[[], T]) -> tuple[T, int]:
start = perf_counter_ns()
result = function()
duration = perf_counter_ns() - start
return result, duration
result, duration = measure(lambda: sorted(range(50_000), reverse=True))
print(result[:3], duration)
A useful benchmark record should also include input size, Python version, platform, and repetition number. Without context, a duration is difficult to compare later.
Timing a block with a context manager
from contextlib import contextmanager
from time import perf_counter_ns
@contextmanager
def timer(label: str):
start = perf_counter_ns()
try:
yield
finally:
duration = perf_counter_ns() - start
print(f"{label}: {duration / 1_000_000:.3f} ms")
with timer("processing"):
values = [x ** 2 for x in range(200_000)]
The finally block records the duration even if the measured code raises an exception.
Why one measurement is not enough
The operating system schedules processes, caches warm up, garbage collection may run, and other programs compete for CPU, memory, disk, and network resources. A single sample can capture unusual noise. Repeat the operation and inspect a distribution.
from statistics import median
from time import perf_counter_ns
samples = []
for _ in range(30):
start = perf_counter_ns()
sum(range(100_000))
samples.append(perf_counter_ns() - start)
print("median:", median(samples), "ns")
print("minimum:", min(samples), "ns")
The median often represents typical execution better than the mean when outliers exist. The minimum may approximate uninterrupted cost, but it may not represent production latency.
Warm-up and caches
The first execution may pay for imports, object creation, disk reads, DNS resolution, or cache initialization. Run a warm-up phase when your question concerns steady-state performance.
def task():
return sum(i * i for i in range(20_000))
for _ in range(5):
task()
Do not hide startup cost when startup is part of the user experience. Benchmark design must match the real question.
When to use timeit instead
For microbenchmarks, Python’s timeit module is usually safer because it repeats code and provides a purpose-built interface. perf_counter_ns() is excellent for instrumenting real workflows, measuring asynchronous operations, recording request latency, and building custom metrics.
Related guides include measuring Python code with timeit, Python optimization basics, profiling with cProfile, and Python logging.
Asynchronous operations
import asyncio
from time import perf_counter_ns
async def main():
start = perf_counter_ns()
await asyncio.sleep(0.05)
duration = perf_counter_ns() - start
print(duration / 1_000_000, "ms")
asyncio.run(main())
This duration includes waiting time, which is correct for end-to-end latency. To measure CPU consumption instead, consider process_time_ns().
Choosing the right clock
time_ns() represents calendar time and can be adjusted by the system. monotonic_ns() never moves backward and is suitable for intervals. perf_counter_ns() is also monotonic and uses a high-resolution performance counter. process_time_ns() measures CPU time used by the process and excludes sleep or I/O waiting. Select the clock based on the question you need to answer.
Common mistakes
Do not compare raw counter values from different machines or after a restart. Do not interpret the absolute value as a timestamp. Avoid placing printing, logging, test data construction, or unrelated setup inside the timed section unless those costs are intentionally part of the test.
Another common mistake is drawing conclusions from a tiny difference and only a few samples. Use realistic input sizes, repeat the test, and report variation. Performance depends on data distribution, cache state, storage, network, concurrency, Python build, and hardware.
Latency percentiles
Production averages can hide slow tails. Store many durations and calculate percentiles such as p50, p95, and p99. An API may have a low mean while still delivering poor experiences to a meaningful fraction of users. Keep the raw values in nanoseconds or convert them to milliseconds when sending metrics.
Benchmarking best practices
Define the operation boundary clearly. Warm up only when appropriate. Use multiple repetitions and representative data. Separate CPU time from total latency. Compare results in the same environment. Record software versions and system configuration. Most importantly, profile before optimizing so that you spend effort on a real bottleneck.
The primary reference is the official Python time documentation. For controlled microbenchmarks, consult the official timeit documentation.
Conclusion
perf_counter_ns() is a simple, powerful tool for high-resolution duration measurement with integer values. It works well for latency instrumentation, timing helpers, context managers, and custom performance experiments. Reliable results, however, depend more on methodology than on clock precision. Repeat measurements, control the environment, use realistic workloads, choose appropriate statistics, and explain what was included in the measured interval. These practices turn nanosecond readings into much safer optimization decisions.







