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.
6 min read
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:
uuid4, string/hex/bytes forms.secrets.token_bytes / token_hex / token_urlsafe at 16 and 32 bytes.os.urandom for comparison.uuid1 as a labeled non-recommendation.Uniqueness / collisions are not measured — throughput only.
Related links:
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×); 8.3×). Formatting (UUID object → str) has a real tax.os.urandom(16) ~8.06M (
| Call | Typical product | Notes |
|---|---|---|
uuid.uuid4() | UUID object | RFC 4122 v4 |
str(uuid4()) / .hex | text forms | extra formatting |
secrets.token_bytes(n) | raw bytes | CSPRNG wrapper |
secrets.token_hex(n) | 2n hex chars | convenient |
secrets.token_urlsafe(n) | base64url text | web-safe |
os.urandom(n) | raw bytes | lowest wrapper |
uuid.uuid1() | time/node UUID | not for secrets |
Related links:
Script: lab-evidence/41-uuid4-vs-secrets/results/run_lab.py.
| Arm | ops/s | ns/op | vs uuid4 |
|---|---|---|---|
uuid.uuid4() | 972 233 | 1029 | 1.00× |
uuid4().bytes | 912 121 | 1096 | 0.94× |
uuid4().hex | 687 732 | 1454 | 0.71× |
str(uuid4()) | 554 568 | 1803 | 0.57× |
secrets.token_urlsafe(16) | 2 097 648 | 477 | 2.16× |
secrets.token_hex(16) | 4 291 700 | 233 | 4.41× |
secrets.token_bytes(16) | 5 684 152 | 176 | 5.85× |
os.urandom(16) | 8 058 339 | 124 | 8.29× |
uuid.uuid1() | 219 553 | 4555 | 0.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:
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.
uuid4 (accept ~1 µs/op class).secrets.token_urlsafe or token_hex (faster than uuid4 here).secrets.token_bytes or os.urandom.uuid1 is slower and leakier — do not use for session secrets.Related links:
str(uuid4()) tax (~1.8× slower than bare uuid4).secrets module APIs.uuid4, store/transmit the form you need (object vs hex vs str).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:
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.
uuid, secrets, os.urandom)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 2.1M/s) on equal footing — same “string out” requirement. Conversely, if Postgres/token_urlsafe(16) (UUID columns accept binary/UUID types, skipping str() keeps uuid4 closer to its ceiling.
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.
| API | Entropy bytes | Text length (approx) |
|---|---|---|
token_bytes(16) | 16 | n/a (binary) |
token_hex(16) | 16 | 32 hex chars |
token_urlsafe(16) | 16 | ~22 chars |
token_bytes(32) | 32 | binary |
uuid4 | 122 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.
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.
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.
Lab evidence
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/.
Lab1 Oct 20264 min read
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
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