ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 40

  1. Blog

uuid4 vs secrets Token Generation Rate Lab

Hands-on uuid.uuid4 vs secrets.token_hex/urlsafe vs os.urandom ops/s on localhost. Throughput only; uniqueness and collisions not measured in this lab.

Aditya Challa·30 September 2026·6 min read

Summary
On this page
  1. Intro — what this post promises
  2. What each call gives you
  3. Lab topology
  4. Lead table — p50 ops/s
  5. Stability notes
  6. How to read these numbers
  7. Pitfalls we hit (or avoided)
  8. Practical checklist
  9. Methodology footnote
  10. Versions pinned
  11. Formatting is part of the API
  12. What we deliberately did not do
  13. Append: entropy size checklist
  14. Related reading on this blog
  15. Verdict

Intro — what this post promises

Session IDs, CSRF tokens, and job keys all call some RNG wrapper. uuid.uuid4() is ubiquitous; secrets is the stdlib module meant for security tokens; os.urandom is the underlying entropy source. How many can you mint per second?

This is a hands-on lab with measured numbers:

  1. Ops/s for uuid4, string/hex/bytes forms.
  2. secrets.token_bytes / token_hex / token_urlsafe at 16 and 32 bytes.
  3. Raw os.urandom for comparison.
  4. uuid1 as a labeled non-recommendation.

Uniqueness / collisions are not measured — throughput only.

Related links:

  • SHA-256 vs BLAKE2b vs xxHash localhost lab
  • base64 vs hex encode localhost lab
  • json vs orjson vs msgpack localhost lab
  • fork COW RSS vs spawn localhost lab
  • Why your average latency graph is lying (p50 / p95 / p99)

Lab honesty (1 Oct 2026 IST): Shared Linux lab box. Python 3.13.5. Affiliates: 0. This is not a cryptographic proof and not a birthday-bound study.

Verdict up front: uuid4 ~972k ops/s; secrets.token_hex(16) ~4.29M (4.4×); os.urandom(16) ~8.06M (8.3×). Formatting (UUID object → str) has a real tax.


What each call gives you

CallTypical productNotes
uuid.uuid4()UUID objectRFC 4122 v4
str(uuid4()) / .hextext formsextra formatting
secrets.token_bytes(n)raw bytesCSPRNG wrapper
secrets.token_hex(n)2n hex charsconvenient
secrets.token_urlsafe(n)base64url textweb-safe
os.urandom(n)raw byteslowest wrapper
uuid.uuid1()time/node UUIDnot for secrets

Related links:

  • SHA-256 vs BLAKE2b vs xxHash localhost lab

Lab topology

Batch: 200_000 calls (100_000 for 32-byte arms)
Metric: p50 batch wall → ops/s and ns/op
Warmup short loop; 7 timed repeats

Script: lab-evidence/41-uuid4-vs-secrets/results/run_lab.py.


Lead table — p50 ops/s

Armops/sns/opvs uuid4
uuid.uuid4()972 23310291.00×
uuid4().bytes912 12110960.94×
uuid4().hex687 73214540.71×
str(uuid4())554 56818030.57×
secrets.token_urlsafe(16)2 097 6484772.16×
secrets.token_hex(16)4 291 7002334.41×
secrets.token_bytes(16)5 684 1521765.85×
os.urandom(16)8 058 3391248.29×
uuid.uuid1()219 55345550.23×

32-byte arms (100k batches): token_bytes ~4.52M, token_hex ~3.54M, token_urlsafe ~1.86M, os.urandom ~6.14M ops/s.

Related links:

  • Why your average latency graph is lying (p50 / p95 / p99)

Stability notes

uuid4 stability ~954k–990k ops/s. token_hex(16) stability showed ~3.3–4.5M (noisier). os.urandom stability ~4.6–5.5M on shorter 100k batches vs ~8.1M primary — quote bands, lead with the primary 200k-op table, and avoid fake precision.


How to read these numbers

  • Need a UUID type / RFC form → uuid4 (accept ~1 µs/op class).
  • Need a security token string → secrets.token_urlsafe or token_hex (faster than uuid4 here).
  • Need raw key material → secrets.token_bytes or os.urandom.
  • uuid1 is slower and leakier — do not use for session secrets.
  • Throughput ≠ uniqueness proof.

Related links:

  • base64 vs hex encode localhost lab
  • json vs orjson vs msgpack localhost lab
  • fork COW RSS vs spawn localhost lab

Pitfalls we hit (or avoided)

  1. Claiming collision rates from ops/s — explicitly out of scope.
  2. Using uuid1 for tokens because it “looks random.”
  3. Ignoring str(uuid4()) tax (~1.8× slower than bare uuid4).
  4. Comparing 16-byte hex to 32-byte tokens without labeling entropy size.
  5. Single-run urandom spikes — stability bands matter under load.

Practical checklist

  • Security-sensitive IDs: prefer secrets module APIs.
  • Public UUID fields: uuid4, store/transmit the form you need (object vs hex vs str).
  • Measure formatting cost if you mint millions/sec.
  • Document token byte length (16 vs 32).
  • Never treat this lab as a CSPRNG audit.

If you also base64/hex encode tokens for cookies, add that cost from the companion encode lab — token_urlsafe already includes a web-safe encode step (and still beat uuid4 here).

Related links:

  • fsync vs fdatasync localhost lab
  • SQLite WAL vs DELETE journal localhost lab
  • python re vs str methods localhost lab
  • openssl TLS full vs resume localhost lab

Methodology footnote

Each timed trial runs n_ops synchronous calls in a tight Python loop (200 000 default; 100 000 for 32-byte arms). Ops/s = n_ops / p50_seconds. Samples are retained only for repr length checks — not for uniqueness analysis.


Versions pinned

  • Python 3.13.5 (uuid, secrets, os.urandom)
  • Default entropy size 16 bytes unless labeled 32

Formatting is part of the API

Bare uuid4() returns a UUID object (~972k/s). Most apps immediately need text:

  • .hex → ~688k/s (0.71×)
  • str(...) → ~555k/s (0.57×)

If your hot path mints string IDs, compare against secrets.token_hex(16) (4.3M/s) or token_urlsafe(16) (2.1M/s) on equal footing — same “string out” requirement. Conversely, if Postgres/UUID columns accept binary/UUID types, skipping str() keeps uuid4 closer to its ceiling.

What we deliberately did not do

No multi-threaded minting, no /dev/random blocking tests, no chi-square on outputs, no distributed collision hunt. Under cgroup CPU throttling your absolute ops/s will move; the ranking (urandom/token_bytes ≫ token_hex ≫ uuid4 ≫ uuid1) is the portable lesson until re-measured.

Append: entropy size checklist

APIEntropy bytesText length (approx)
token_bytes(16)16n/a (binary)
token_hex(16)1632 hex chars
token_urlsafe(16)16~22 chars
token_bytes(32)32binary
uuid4122 bits random (6 bits version/variant)36 chars with dashes

Minting faster does not add bits. A 16-byte token at 4 M ops/s is still 128 bits of entropy per token — the lab only says you can ask the CSPRNG that often. For cookie headers, prefer token_urlsafe; for debugging dumps, token_hex; for key material, token_bytes / urandom.

Under a web worker minting IDs per request, even uuid4’s ~1 µs is rarely the p95 villain — but batch jobs creating tens of millions of rows should still pick the API that matches the column type and security story, not cargo-cult uuid4.

Related reading on this blog

Token minting often sits beside TLS session setup and DB inserts (openssl / SQLite WAL labs). If minting is fast and INSERT is slow, optimize the durable path first — this ops/s table only clears the RNG wrapper as a bottleneck.

Verdict

On this box, bare uuid4 (~972k ops/s) trails secrets.token_hex(16) (~4.29M) and os.urandom(16) (~8.06M) by several×, while str(uuid4()) (~555k) shows formatting cost. Use UUID when you need UUID; use secrets when you need tokens — and remember we did not measure collisions.

Evidence path on the lab box: lab-evidence/41-uuid4-vs-secrets/results/. Affiliates: 0.

uuid4 performancesecrets.token_hextoken_urlsafeos.urandompython tokenslocalhost labsreid generation

Lab evidence

What I found running this

Lab 1 Oct 2026 IST. Python 3.13.5; 200k ops batches. uuid4 ~972k ops/s (~1029 ns); uuid4 str ~555k; secrets.token_bytes(16) ~5.68M (~5.8x); token_hex(16) ~4.29M (~4.4x); token_urlsafe(16) ~2.10M (~2.2x); os.urandom(16) ~8.06M (~8.3x). uuid1 ~220k (not a secrets pick). Uniqueness/collisions not measured. Affiliates: 0. Evidence: lab-evidence/41-uuid4-vs-secrets/.

Notes when a lab post goes up

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

Related links

  • Plate 09

    secrets vs urandom vs getrandbits: Localhost Lab

    Observability & SRE · 30 Sept 2026

  • Plate 17

    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.

    1 Oct 2026

  • Plate 75

    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.

    1 Oct 2026

On this page

  1. Intro — what this post promises
  2. What each call gives you
  3. Lab topology
  4. Lead table — p50 ops/s
  5. Stability notes
  6. How to read these numbers
  7. Pitfalls we hit (or avoided)
  8. Practical checklist
  9. Methodology footnote
  10. Versions pinned
  11. Formatting is part of the API
  12. What we deliberately did not do
  13. Append: entropy size checklist
  14. Related reading on this blog
  15. Verdict
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove