Clock-free, persistent, reversible-permutation 64-bit ID generation
Project description
permid64
Clock-free, persistent, reversible-permutation 64-bit ID generation.
Counter in, permutation out.
permid64 generates unique 64-bit integer IDs without relying on wall-clock time. It combines a crash-safe persistent counter with an invertible permutation to produce IDs that look random but carry recoverable metadata.
# Raw counter (leaks business volume at a glance)
1001, 1002, 1003 ...
# permid64 (shuffled surface, recoverable structure)
12609531668580943872, 7349201938475629, 3847291038012847 ...
# decode(12609531668580943872) → instance_id=42, sequence=0
Why permid64?
| Problem | permid64's answer |
|---|---|
| Clock skew / NTP jumps break time-based IDs | Counter, not clock |
| Monotonic counters leak business volume | Permutation hides raw counter |
| IDs must survive process restarts | State file with block reservation |
| IDs must be decodable (audit, debug) | decode() reverses the permutation |
| No infrastructure dependency | Pure Python, zero runtime deps |
permid64 vs Snowflake vs UUID
| permid64 | Snowflake ID | UUID v4 | |
|---|---|---|---|
| Bit width | 64 | 64 | 128 |
| Core dependency | Persistent counter (file) | System clock (NTP) | Random source |
| Clock rollback risk | None | High | None |
| External appearance | Shuffled (random-looking) | Roughly increasing | Random |
| DB index friendliness | Moderate (random writes) | Excellent (sequential) | Poor (random writes) |
| Decodable | Yes (instance_id + sequence) |
Yes (timestamp + worker_id) |
No |
| Infrastructure needed | Local durable storage | Worker ID coordination (ZK/etcd) | None |
| Best for | External-facing IDs, anti-scraping | Internal logs, time-ordered streams | Globally unique tokens |
What it is
- A clock-free 64-bit ID generator — no timestamp, no NTP dependency
- IDs are unique because the source is a monotonically increasing counter
- IDs look shuffled because they pass through a reversible permutation
- The permutation is invertible —
decode()recovers the original metadata
What it is not
- Not a timestamp-based scheme — there is no time component in the ID
- Not a UUID replacement for every scenario — if you need a globally unique random token with no infrastructure at all, UUID v4 is simpler
- Not cryptographic encryption — the permutation is an obfuscation layer, not authenticated encryption; do not use IDs as secrets or security tokens
PersistentCounterSourceis single-process only — for Gunicorn prefork, uWSGI, ormultiprocessing, useProcessSafeCounterSource(see Multi-process deployment)
Installation
pip install permid64
For development (tests, linting, type checking):
pip install -e ".[dev]"
Quick start
from permid64 import Id64
# Multiplicative (fastest)
gen = Id64.multiplicative(
instance_id=42,
state_file="permid64.state",
block_size=4096,
)
uid = gen.next_u64() # e.g. 12609531668580943872
meta = gen.decode(uid)
# DecodedId(raw=2748779069440, instance_id=42, sequence=0)
print(meta.instance_id, meta.sequence)
# Feistel (better statistical mixing)
gen2 = Id64.feistel(
instance_id=42,
state_file="permid64.state",
block_size=4096,
key=0xDEADBEEFCAFEBABE,
rounds=6,
)
String tokens (Base62 and Crockford Base32)
For order numbers, invite codes, and shareable URLs, fixed-width alphanumeric tokens are easier to read than a 19-digit decimal. They encode the same next_u64() value — no second ID space.
# 11 chars: 0-9, A-Z, a-z (case-sensitive in URLs and logs)
token = gen.next_base62()
meta = gen.decode_base62(token)
# 13 chars: Crockford Base32 (uppercase only; excludes I, L, O, U)
tok32 = gen.next_base32()
meta = gen.decode_base32(tok32)
Stateless integer codecs (for storage or custom pipelines):
from permid64 import u64_to_base62, base62_to_u64
s = u64_to_base62(12345678901234567890)
n = base62_to_u64(s)
Strings are not secrets: anyone who knows the alphabet and (for decode) the same permutation parameters can map tokens back to integers and metadata.
Why decode() matters
In production, when an anomalous ID appears in a log or alert, you can decode it instantly — no DB lookup needed:
meta = gen.decode(12609531668580943872)
print(f"Issued by instance {meta.instance_id}, sequence #{meta.sequence}")
# Issued by instance 42, sequence #0
This makes incident tracing dramatically faster: you immediately know which shard issued the ID and its approximate position in the issuance history.
Assigning instance_id
Assign each process or deployment unit a distinct instance_id. Common patterns:
import os
# From environment variable (works in Docker / K8s)
instance_id = int(os.environ.get("INSTANCE_ID", "0"))
# From K8s StatefulSet pod name (e.g. "worker-3" -> 3)
import re
pod_name = os.environ.get("POD_NAME", "worker-0")
instance_id = int(re.search(r"(\d+)$", pod_name).group(1))
Each instance_id gets its own independent sequence space — no coordination needed between shards.
Design
seq = source.next() # monotonic counter (persistent)
raw = layout.compose(instance_id, seq) # pack 16-bit shard + 48-bit seq
id64 = permutation.forward(raw) # obfuscate with invertible bijection
Layout — default 64-bit split:
[ instance_id : 16 bits ][ sequence : 48 bits ]
- Up to 65 535 independent shards
- Up to 281 trillion IDs per shard
Permutations — both are bijections over [0, 2^64):
| Mode | Formula | Speed | Mixing |
|---|---|---|---|
multiplicative |
f(x) = (a·x + b) mod 2^64 |
~500 M/s | Good |
feistel |
64-bit Feistel network | ~150 M/s | Excellent |
Persistence — block reservation strategy:
- On startup, read high-water mark from state file.
- Reserve a block of N sequence numbers, write new high-water mark.
- Serve IDs from memory until block exhausted.
- If the process crashes, the unused block is lost (gap), but no duplicate is ever issued.
Multi-process deployment
For any deployment where more than one OS process shares the same state file, use ProcessSafeCounterSource:
from permid64 import Id64, ProcessSafeCounterSource
from permid64.permutation import MultiplyOddPermutation
src = ProcessSafeCounterSource("/var/lib/myapp/counter.state", block_size=256)
gen = Id64(
instance_id=1,
source=src,
permutation=MultiplyOddPermutation(),
)
- POSIX:
fcntl.lockf(OFD semantics) — survivesfork()without deadlock - Fork-safe: child processes automatically discard the inherited in-memory block and reserve a fresh one
- NFS/CIFS: emits a
UserWarningonce per process; move the state file to local storage for guaranteed correctness - Windows: best-effort (
msvcrt.locking); use distinct state files per process until v0.5
See MULTI_PROCESS.md for the full deployment guide.
Running tests
pytest
Five acceptance criteria are checked:
- Uniqueness — 1 million IDs, zero duplicates
- Invertibility —
decode(next_u64())recoversinstance_idandsequence - Restart safety — sequence never resets across process restarts
- Gap tolerance — crash causes a gap, never a duplicate
- Thread safety — concurrent generation remains unique
Benchmark
python benchmarks/bench_id64.py
Sample output (Apple M2):
[Permutation comparison — block_size=4096]
multiplicative (default keys) ~480,000,000 IDs/sec
feistel (6 rounds) ~140,000,000 IDs/sec
feistel (12 rounds) ~80,000,000 IDs/sec
Guarantees
| Guarantee | Notes |
|---|---|
| No duplicate IDs within a shard | Strict |
| No duplicates across restarts | Strict — state file must be on durable storage |
| Decodable | Only with the same permutation key / params |
| Gaps allowed | After a crash, some sequence numbers are skipped |
| No global coordination | Each instance_id is fully independent |
Limitations
Multi-process deployments
PersistentCounterSource is not safe for concurrent use across multiple processes sharing the same state file. Use ProcessSafeCounterSource instead — it provides hard multi-process guarantees on POSIX via fcntl.lockf (OFD semantics) and is fork-safe.
On Windows, ProcessSafeCounterSource falls back to best-effort msvcrt.locking. Use distinct state files per process until v0.5.
Feistel is obfuscation, not encryption
The Feistel permutation provides strong mixing and is reversible, but it is not a formally audited cryptographic primitive. Do not rely on it for access control, token authentication, or any security-sensitive use case.
instance_id must be assigned manually
There is no automatic shard coordination. Assign instance_id values via config or environment variables and ensure they are unique across your deployment.
Sequence space is large but finite
The default 48-bit sequence space supports ~281 trillion IDs per shard. This is enough for virtually all workloads, but it is not infinite.
Architecture
permid64/
__init__.py # public exports: Id64, DecodedId, codecs, Id64Config, …
generator.py # Id64 façade
source.py # PersistentCounterSource, ProcessSafeCounterSource
layout.py # Layout64 — pack/unpack 64-bit raw value
permutation.py # MultiplyOddPermutation, Feistel64Permutation, IdentityPermutation
codec.py # fixed-width Base62 / Crockford Base32 for u64
config.py # Id64Config + build_id64 (experimental)
types.py # DecodedId dataclass
tests/
test_counter.py
test_layout.py
test_permutation.py
test_codec.py
test_config.py
test_id64_e2e.py # the 5 MVP acceptance tests
benchmarks/
bench_id64.py
Roadmap
| Version | Theme | What's included | Status |
|---|---|---|---|
| v0.1 | Core primitives | PersistentCounterSource, Feistel / Multiplicative permutation, decode() |
✅ Released |
| v0.2 | Encoding & Config | IdentityPermutation, fixed-width Base62 + Crockford Base32 (next_base62 / decode_base62, next_base32 / decode_base32), Id64Config + build_id64 |
✅ Released |
| v0.3 | Multi-process safety | ProcessSafeCounterSource with fcntl.lockf (OFD semantics), fork-safety PID check, NFS detection, sidecar .lock file, PermId64ConfigError |
✅ Released |
| v0.4 | Human-friendly output | Check digit (+1 char checksum), PrefixedEncoder (ORD_ / TKT_ / …), FormatSpec (segmented display, ambiguity-free charset for manual entry) |
Planned |
| v0.5 | Distributed sources | ReservedBlockSource (central allocator, Redis / PG-backed block rental), instance_id helpers (hostname hash, env var, StatefulSet ordinal) |
Planned |
| v0.6 | Solution presets | OrderIdGenerator, TicketIdGenerator, CorrelationIdGenerator, IoTEventIdGenerator — ready-made recipes for common use cases |
Planned |
| v0.7 | Formal spec | Cross-language bit-layout specification, profile aliases (m1 / f6 / human32), compatibility guarantee document |
Planned |
| v1.0 | Reference impls | Go reference implementation, Rust reference implementation, three-language cross-decode test suite | Planned |
Contributing
Contributions are welcome! Please open an issue first to discuss what you'd like to change.
# Clone and set up dev environment
git clone https://github.com/erickh826/permid64.git
cd permid64
pip install -e ".[dev]"
# Run checks before submitting a PR
pytest tests/ -v
ruff check permid64/ tests/
mypy permid64/ --ignore-missing-imports
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file permid64-0.3.0.tar.gz.
File metadata
- Download URL: permid64-0.3.0.tar.gz
- Upload date:
- Size: 33.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e5b906194ae469ac602eaab115bd6421b9c0de183202e20c2d77e086490a9fc
|
|
| MD5 |
4870f2edcdf6a6b4e78e991279ebaaaf
|
|
| BLAKE2b-256 |
a641424f3abc67be31610969297b57e0ec7b54ca02a4a968a1afac999df0f1e6
|
Provenance
The following attestation bundles were made for permid64-0.3.0.tar.gz:
Publisher:
release.yml on erickh826/permid64
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
permid64-0.3.0.tar.gz -
Subject digest:
2e5b906194ae469ac602eaab115bd6421b9c0de183202e20c2d77e086490a9fc - Sigstore transparency entry: 1301994752
- Sigstore integration time:
-
Permalink:
erickh826/permid64@ab737a582f20b9974d00be24033f40d7261651a9 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/erickh826
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ab737a582f20b9974d00be24033f40d7261651a9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file permid64-0.3.0-py3-none-any.whl.
File metadata
- Download URL: permid64-0.3.0-py3-none-any.whl
- Upload date:
- Size: 22.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c5585c1116a598efd25c5f449c6f51a7e69de34986d55cdfe45aa589eab713fe
|
|
| MD5 |
b8822b3094425752e383fbc92c02d23d
|
|
| BLAKE2b-256 |
7c0bb6d2f3110bb4673a85282274c61200df3bdf77f148ed2245b84d133307fd
|
Provenance
The following attestation bundles were made for permid64-0.3.0-py3-none-any.whl:
Publisher:
release.yml on erickh826/permid64
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
permid64-0.3.0-py3-none-any.whl -
Subject digest:
c5585c1116a598efd25c5f449c6f51a7e69de34986d55cdfe45aa589eab713fe - Sigstore transparency entry: 1301995023
- Sigstore integration time:
-
Permalink:
erickh826/permid64@ab737a582f20b9974d00be24033f40d7261651a9 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/erickh826
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ab737a582f20b9974d00be24033f40d7261651a9 -
Trigger Event:
push
-
Statement type: