ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
Topics
sre111localhost lab102performance70python58javascript47bundle size36benchmarking35node.js35web development21typescript19benchmark16content creation15All topics
About
Loading article…

ShopperCove

Hands-on notes on developer tools, written after the tool has actually been run.

All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove

On this page

  1. Intro — what this post promises
  2. Clocks under test
  3. Lab topology
  4. Clock info (this box)
  5. Lead table — call overhead (p50)
  6. Resolution caveats
  7. Pitfalls
  8. When to pick what
  9. Reproduce
  10. Closing
  11. FAQ
  12. What is time.perf\_counter?
  13. Why not use time.time() for durations?
  14. How precise is perf\_counter really?
  1. Blog

perf_counter vs time.time: Which Python Timer to Use?

A hands-on Linux localhost lab measuring Python clock-call overhead, observed resolution, and safe choices for elapsed-time measurements.

30 September 2026·5 min read

Summary

Disclosure

This post contains affiliate links. If you buy through them, I may earn a commission at no extra cost to you. I only make firsthand claims about products I have actually used.

On this page
  1. Intro — what this post promises
  2. Clocks under test
  3. Lab topology
  4. Clock info (this box)
  5. Lead table — call overhead (p50)
  6. Resolution caveats
  7. Pitfalls
  8. When to pick what
  9. Reproduce
  10. Closing
  11. FAQ
  12. What is time.perf\_counter?
  13. Why not use time.time() for durations?
  14. How precise is perf\_counter really?

Intro — what this post promises

Use time.perf_counter to measure elapsed time in Python: it is monotonic, so NTP adjustments cannot move it, while time.time is a wall clock that can step forward or back and belongs in logs and timestamps. Cost doesn't decide it, because perf_counter, monotonic and time all took about 58-60 ns per call on Python 3.13.5 (about 33 ns net of loop overhead), and only process_time was much slower at about 278 ns.

Related links:

  • 01shoppercove.comlru_cache hit vs miss localhost lab
  • 02shoppercove.comlogging vs print localhost lab
  • 03shoppercove.comdeque vs list queue localhost lab
  • 04shoppercove.comitertools vs python loops localhost lab
  • 05shoppercove.comWhy your average latency graph is lying (p50 / p95 / p99)
  • 06shoppercove.comcopy vs deepcopy localhost lab
  • 07shoppercove.comstruct pack vs to_bytes localhost lab
  • 08shoppercove.comset vs list membership localhost lab

Lab honesty (1 Oct 2026 IST): Python 3.13.5. 500,000 calls/batch. Affiliates: 0. Overhead includes the for loop; we also report net vs an empty xor loop. Resolution claims are observed min positive delta, not a kernel guarantee under load.

Verdict up front: perf_counter / monotonic / time are peers at ~58–60 ns/call. process_time is the outlier (~278 ns). Empty loop baseline ~25 ns → net timer cost ~33 ns. Prefer perf_counter for intervals; avoid time() for durations (wall clock can jump).


Clocks under test

APIRole
perf_counter / _nshigh-res monotonic intervals
monotonic / _nsmonotonic (here same CLOCK_MONOTONIC)
time / _nswall clock
process_timeCPU time

Lab topology

N = 500000 calls / repeat, 7 repeats, p50
Also: empty xor loop; t1-t0 pair pattern
get_clock_info + min positive back-to-back delta

Script: lab-evidence/57-perf-counter-vs-time/results/run_lab.py.


Clock info (this box)

Clockresolutionmonotonicimplementation
time1e-09Falseclock_gettime(CLOCK_REALTIME)
monotonic1e-09Trueclock_gettime(CLOCK_MONOTONIC)
perf_counter1e-09Trueclock_gettime(CLOCK_MONOTONIC)
process_time1e-09Trueclock_gettime(CLOCK_PROCESS_CPUTIME_ID)

Reported resolution is 1 ns for all four via clock_gettime. That is the API’s claimed resolution — not “every call returns a new nanosecond.”


Lead table — call overhead (p50)

Armcalls/sns/callnet vs empty (ns)
empty xor loop40,196,92324.90
perf_counter17,293,53757.832.9
monotonic17,245,70758.033.1
time16,751,35159.734.8
perf_counter_ns16,054,41162.337.4
monotonic_ns16,171,90161.837.0
time_ns16,185,06861.836.9
process_time3,592,895278.3253.4
perf_counter pair Δ17,167,53158.233.4

Resolution caveats

Observed minimum positive back-to-back delta (10k trials):

Clockmin +Δ
perf_counter (s)4.599860403686762e-08
monotonic (s)4.3997715692967176e-08
time (s)2.384185791015625e-07
perf_counter_ns46 ns
time_ns48 ns

So: do not treat sub-100 ns interval measurements as meaningful on this box — call overhead and clock grain are in that band. For microbenchmarks, batch work and divide, or accept that noise floors matter.

time() can step backward/forward with NTP; never use it for elapsed = t1 - t0 in production timers. perf_counter (or monotonic) is the interval API.


Pitfalls

  1. Timing a single call — overhead ≈ the thing you timed.
  2. Using time.time for durations — wall clock, adjustable.
  3. Trusting resolution=1e-9 as usable precision — see min +Δ.
  4. process_time for wall latency — ignores sleep/IO wait; also slower to call here.

When to pick what

NeedPrefer
Elapsed wall intervalstime.perf_counter
Deadlines immune to NTPtime.monotonic
Logs / absolute timestampstime.time / time_ns
CPU-only profilingprocess_time

Reproduce

python3 lab-evidence/57-perf-counter-vs-time/results/run_lab.py

Evidence: /workspace/lab-evidence/57-perf-counter-vs-time/results/.


Closing

Timer calls cost tens of nanoseconds. On this box perf_counter ≈ monotonic ≈ time at ~58 ns/call; process_time ~278 ns. Net after an empty loop ~33 ns. Use perf_counter for intervals; batch when measuring work near that floor.

FAQ

What is time.perf_counter?

Python's high-resolution monotonic clock for measuring intervals. On this Linux box it used clock_gettime(CLOCK_MONOTONIC) with a reported resolution of 1 ns.

Why not use time.time() for durations?

time.time is wall-clock time and can jump when NTP adjusts the clock, so t1 - t0 can be wrong. Use perf_counter or monotonic for elapsed time.

How precise is perf_counter really?

The smallest positive back-to-back delta observed was about 46 ns, so sub-100 ns timings are noise. Batch the work and divide instead of timing a single call.

time.perf_countertime.timetime.monotonictimer overheadpython timinglocalhost labsrebenchmark

Lab evidence

What I found running this

Lab 1 Oct 2026 IST. Ran Python 3.13.5 with 500,000 calls per batch. Empty xor loop 24.9 ns; perf_counter 57.8 ns/call, monotonic 58.0, time 59.7, process_time 278. Net perf_counter cost about 33 ns. min positive perf_counter_ns delta 46 ns. Affiliates: 0. Evidence: lab-evidence/57-perf-counter-vs-time/.

Explore the product

Untested by us. Check what's included, the license and refund terms before you buy.

Ready Client Sites for prebuilt websites

See Ready Client Sites details

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

Explore the product

Untested by us. Check what's included, the license and refund terms before you buy.

Ready Client Sites for prebuilt websites

See Ready Client Sites details

Keep reading

  • platform vs os.uname Inventory: Localhost Lab

    Hands-on platform.platform vs os.uname host inventory lab: real ops/s plus cache notes, measured on Linux localhost today in this hands-on lab for SREs.

    Summary1 Oct 20264 min read

  • uuid.uuid4 vs uuid.uuid1: Localhost Lab

    Hands-on uuid.uuid4 vs uuid.uuid1 ID generation lab: real ops/s plus version/node checks, measured on Linux localhost today in this hands-on lab for SREs.

    Summary1 Oct 20263 min read

  • signal vs threading.Event Wakeup: Localhost Lab

    Hands-on signal SIGUSR1 vs threading.Event wakeup lab: real p50 latency in microseconds, measured on Linux localhost today in this hands-on lab for SREs.

    Summary1 Oct 20264 min read