perf_counter_ns: Measure Performance in Nanoseconds

Published on: September 20, 2026
Reading time: 4 minutes
Python code being measured for performance with perf_counter_ns

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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Python code representing a priority queue with heapq max-heap
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    heapq Max-Heap: Max-Priority Queues in Python

    Learn Python heapq max-heap functions for priority queues, rankings, schedulers, and efficient selection algorithms.

    Ler mais

    Tempo de leitura: 4 minutos
    19/09/2026
    Server infrastructure representing parallel ProcessPoolExecutor workers
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    ProcessPoolExecutor kill_workers: Stop Stuck Processes

    Learn terminate_workers and kill_workers in ProcessPoolExecutor to stop stuck Python processes safely and handle pending futures.

    Ler mais

    Tempo de leitura: 6 minutos
    19/09/2026
    Command-line terminal used by a Python argparse application
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    argparse suggest_on_error: Better CLI Errors

    Learn Python argparse suggest_on_error to recommend valid choices, improve CLI errors, preserve strict validation, and support multiple versions.

    Ler mais

    Tempo de leitura: 5 minutos
    18/09/2026
    Code and file structure for Python Zstandard compression
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    compression.zstd: Compress Data with Zstandard

    Learn how to compress and decompress data with Python compression.zstd using streams, dictionaries, limits, and safe workflows.

    Ler mais

    Tempo de leitura: 5 minutos
    18/09/2026
    Programmer managing an asynchronous queue with Python asyncio.Queue.shutdown
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    asyncio.Queue.shutdown: Shut Down Queues Safely

    Learn Python asyncio.Queue.shutdown to close queues, release workers, drain tasks, and prevent deadlocks in asynchronous pipelines.

    Ler mais

    Tempo de leitura: 5 minutos
    17/09/2026
    Debugging a running Python process with pdb -p
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python pdb -p: Debug Running Processes

    Learn how to use Python pdb -p to attach to running processes, inspect stacks, diagnose hangs, and debug production incidents

    Ler mais

    Tempo de leitura: 6 minutos
    17/09/2026