{"id":26518690,"url":"https://github.com/scru128/spec","last_synced_at":"2026-01-16T06:59:21.861Z","repository":{"id":152307465,"uuid":"418104823","full_name":"scru128/spec","owner":"scru128","description":"SCRU128 Specification","archived":false,"fork":false,"pushed_at":"2023-09-23T12:22:01.000Z","size":44,"stargazers_count":76,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-07-25T03:36:30.587Z","etag":null,"topics":["guid","identifier","ksuid","ulid","uuid"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"cc-by-4.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/scru128.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2021-10-17T11:14:24.000Z","updated_at":"2024-04-10T18:20:45.000Z","dependencies_parsed_at":null,"dependency_job_id":"54fd5a48-c657-4ed7-b45b-c696619faf62","html_url":"https://github.com/scru128/spec","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scru128%2Fspec","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scru128%2Fspec/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scru128%2Fspec/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/scru128%2Fspec/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/scru128","download_url":"https://codeload.github.com/scru128/spec/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244776289,"owners_count":20508505,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["guid","identifier","ksuid","ulid","uuid"],"created_at":"2025-03-21T10:01:31.878Z","updated_at":"2026-01-16T06:59:21.827Z","avatar_url":"https://github.com/scru128.png","language":"C","funding_links":[],"categories":["C"],"sub_categories":[],"readme":"# SCRU128: Sortable, Clock and Random number-based Unique identifier\n\nSCRU128 ID is yet another attempt to supersede [UUID] for the users who need\ndecentralized, globally unique time-ordered identifiers. SCRU128 is inspired by\n[ULID] and [KSUID] and has the following features:\n\n- 128-bit unsigned integer type\n- Sortable by generation time (as integer and as text)\n- 25-digit case-insensitive textual representation (Base36)\n- 48-bit millisecond Unix timestamp that ensures useful life until year 10889\n- Up to 281 trillion time-ordered but unpredictable unique IDs per millisecond\n- 80-bit [three-layer randomness](#design-notes-three-layer-randomness) for\n  global uniqueness\n\nExamples in the 25-digit canonical textual representation:\n\n```\n0372hg16csmsm50l8dikcvukc\n0372hg16csmsm50l8djl6xi25\n0372hg16csmsm50l8dmgepzz1\n0372hg16csmsm50l8doir3827\n0372hg16cy3nowracls909wcd\n0372hg16cy3nowraclvp355ce\n0372hg16cy3nowraclxf2ctzh\n0372hg16cy3nowraclyunyjke\n```\n\nSCRU128's size (128 bits) might not fit in some use cases because of storage\nefficiency. If you need compact, time-ordered unique identifiers generated by\ndistributed nodes, consider [SCRU64]. See the following comparison table for the\nproperties of the two schemes.\n\n|                             | [SCRU64]                                                       | SCRU128                                                   |\n| --------------------------- | -------------------------------------------------------------- | --------------------------------------------------------- |\n| Long name                   | Sortable, Clock-based, Realm-specifically Unique identifier    | Sortable, Clock and Random number-based Unique identifier |\n| Binary size                 | 63 bits                                                        | 128 bits                                                  |\n| Textual size                | 12 digits                                                      | 25 digits                                                 |\n| Textual encoding            | Base36 with `0-9a-z` (case-insensitive)                        | Base36 with `0-9a-z` (case-insensitive)                   |\n| Timestamp range             | January 1, 1970 - February 27, 4261 (UTC)                      | January 1, 1970 - August 2, 10889 (UTC)                   |\n| Timestamp resolution        | 256 milliseconds                                               | 1 millisecond                                             |\n| Number of IDs per timestamp | Up to 8.4 million per 256 milliseconds per node (configurable) | 140.7 trillion per millisecond per node (on average)      |\n| Number of distributed nodes | Up to 8.4 million (configurable)                               | No specific limitation                                    |\n| Source of uniqueness        | Centrally pre-assigned node ID                                 | Independently generated random numbers                    |\n| Choose it when you ...      | Prefer 64-bit integer for storage, indexing, and other reasons | Want unique IDs without hassle to coordinate generators   |\n\n[UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier\n[ULID]: https://github.com/ulid/spec\n[KSUID]: https://github.com/segmentio/ksuid\n[SCRU64]: https://github.com/scru64/spec\n\n## Implementations\n\n- [C/C++](https://github.com/scru128/c)\n- [Go](https://github.com/scru128/go-scru128)\n- [Java (with Kotlin and Android compatibility)](https://github.com/scru128/java)\n- [JavaScript](https://github.com/scru128/javascript)\n- [Python (and command-line tools)](https://github.com/scru128/python)\n- [Rust](https://github.com/scru128/rust)\n- [Swift](https://github.com/scru128/swift-scru128)\n\nIf you are interested in implementing SCRU128, see also [SCRU128 Generator\nTester](https://github.com/scru128/gen_test).\n\n## Specification v2.1.1\n\nA SCRU128 ID is a 128-bit unsigned integer consisting of four terms:\n\n```r\ntimestamp * 2^80 + counter_hi * 2^56 + counter_lo * 2^32 + entropy\n```\n\nWhere:\n\n- `timestamp` is a 48-bit Unix timestamp in milliseconds (i.e., milliseconds\n  elapsed since 1970-01-01 00:00:00+00:00, ignoring leap seconds).\n- `counter_hi` is a 24-bit randomly initialized counter that is incremented by\n  one when `counter_lo` reaches its maximum value. `counter_hi` is reset to a\n  random number when `timestamp` has moved forward by one second or more since\n  the last renewal of `counter_hi`.\n  - Note: `counter_hi` effectively works like an entropy component (rather than\n    a counter) that is refreshed only once per second.\n- `counter_lo` is a 24-bit randomly initialized counter that is incremented by\n  one for each new ID generated within the same `timestamp`. `counter_lo` is\n  reset to a random number whenever `timestamp` moves forward. When `counter_lo`\n  reaches its maximum value, `counter_hi` is incremented and `counter_lo` is\n  reset to zero.\n- `entropy` is a 32-bit random number renewed for each new ID generated.\n\nThis definition is equivalent to allocating four unsigned integer fields to a\n128-bit space according to the following layout:\n\n| Bit numbers  | Field name | Size    | Data type        | Byte order |\n| ------------ | ---------- | ------- | ---------------- | ---------- |\n| Msb 0 - 47   | timestamp  | 48 bits | Unsigned integer | Big-endian |\n| Msb 48 - 71  | counter_hi | 24 bits | Unsigned integer | Big-endian |\n| Msb 72 - 95  | counter_lo | 24 bits | Unsigned integer | Big-endian |\n| Msb 96 - 127 | entropy    | 32 bits | Unsigned integer | Big-endian |\n\nNote that this specification does not specify a canonical bit layout of SCRU128\nID. An implementation may employ any binary form of a 128-bit unsigned integer\nto represent a SCRU128 ID.\n\n### Textual representation\n\nA SCRU128 ID is encoded in a string using the _Base36_ encoding. The Base36\ndenotes a SCRU128 ID as a 128-bit unsigned integer in the radix of 36 using the\ndigits of `0-9a-z` (`0123456789abcdefghijklmnopqrstuvwxyz`), with leading zeros\nadded to form a 25-digit canonical representation. The following pseudo equation\nillustrates the encoding algorithm:\n\n```\n1993501768880490086615869617690763354\n    =  0  * 36^24 +  3  * 36^23 +  7  * 36^22 + ... + 27  * 36^2 + 29  * 36^1 + 22\n    = '0' * 36^24 + '3' * 36^23 + '7' * 36^22 + ... + 'r' * 36^2 + 't' * 36^1 + 'm'\n    = \"0372ijojuxuhjsfkeryi2mrtm\"\n```\n\nAlthough a 25-digit Base36 numeral can encode more than 128-bit information, any\nnumeral greater than `f5lxx1zz5pnorynqglhzmsp33` (`2^128 - 1`, the largest\n128-bit unsigned integer) is not a valid SCRU128 ID.\n\nFor the sake of uniformity, an encoder should use lowercase letters in encoding\nIDs. A decoder, on the other hand, must always ignore cases when interpreting or\nlexicographically sorting encoded IDs.\n\nThe Base36 encoding shown above is available by default in several languages\n(e.g., `BigInteger#toString(int radix)` and `BigInteger(String val, int radix)`\nconstructor in Java). Another easy way to implement it is by using 128-bit or\narbitrary-precision integer division and modulo operations. The following C code\nillustrates a naive algorithm based on normal arrays and integers:\n\n```c\nconst uint8_t id[16] = {1,   127, 239, 57, 194, 100, 27,  165,\n                        106, 148, 131, 24, 136, 65,  224, 90};\n\n// convert byte array into digit value array\nuint8_t digit_values[25] = {0};\nfor (int i = 0; i \u003c 16; i++) {\n  unsigned int carry = id[i];\n  for (int j = 24; j \u003e= 0; j--) {\n    carry += digit_values[j] * 256;\n    digit_values[j] = carry % 36;\n    carry = carry / 36;\n  }\n}\n\n// convert digit value array into string\nstatic const char digits[] = \"0123456789abcdefghijklmnopqrstuvwxyz\";\nchar text[26];\nfor (int i = 0; i \u003c 25; i++) {\n  text[i] = digits[digit_values[i]];\n}\ntext[25] = '\\0';\nputs(text); // 0372ijojuxuhjsfkeryi2mrtm\n```\n\nSee [the attached reference code] for a comprehensive example and test vectors.\n\n[the attached reference code]: https://github.com/scru128/spec/blob/v2.1.1/base36_128.c\n\n### Special-purpose IDs\n\nThe IDs with `timestamp` set at zero or `2^48 - 1` are reserved for special\npurposes (e.g., use as dummy, error, or example values) and must not be used or\nassigned as an identifier of anything.\n\n### Considerations\n\n#### Quality of random numbers\n\nA generator should employ a cryptographically strong random or pseudorandom\nnumber generator to generate unpredictable IDs.\n\n#### Counter overflow handling\n\nCounter overflow occurs at an extremely low probability when the randomly\ninitialized `counter_hi` and `counter_lo` do not provide sufficient space for\nthe IDs generated within a millisecond. The recommended approach to handle\ncounter overflow is to increment `timestamp` and continue in the following way:\n\n1.  Increment `timestamp` by one.\n2.  Reset `counter_hi` to zero.\n3.  Reset `counter_lo` to a random number.\n\nThis approach is recommended over other options such as the \"sleep till next\ntick\" approach because this technique allows the generation of monotonically\nordered IDs in a non-blocking manner. Raising an error on a counter overflow is\ngenerally not recommended because a counter overflow is not a fault of users of\nSCRU128.\n\nThis approach results in a greater `timestamp` value than the real-time clock.\nSuch a gap between `timestamp` and the wall clock should be handled as a small\nclock rollback discussed below.\n\n#### Clock rollback handling\n\nA SCRU128 generator relies on a real-time clock to ensure the monotonic order of\ngenerated IDs; therefore, it cannot guarantee monotonicity when the clock moves\nback. When a generator detects a clock rollback by comparing the up-to-date\ntimestamp from the system clock and the one embedded in the last generated ID,\nthe recommended treatment is:\n\n1.  If the rollback is small enough (e.g., a few seconds), treat the `timestamp`\n    of the last generated ID as the up-to-date one, betting that the wall clock\n    will catch up soon.\n2.  Otherwise, reset `timestamp` to the wall clock and `counter_hi` and\n    `counter_lo` to random numbers if the monotonic order of IDs is not\n    critically important, or raise an error if it is.\n\nThis approach keeps the monotonic order of IDs when a clock rollback is small,\nwhile it otherwise resets the generator and proceeds as if another new generator\nwere created to minimize the chance of collision.\n\n#### Stateless variant\n\nA generator may fill `counter_hi` and `counter_lo` with random numbers if it\ngenerates IDs infrequently. Such a stateless implementation is acceptable,\nthough not recommended, because the outcome is not distinguishable from\ncompliant IDs.\n\n### Design notes: three-layer randomness\n\nSCRU128 utilizes timestamps and counters to ensure the uniqueness of IDs\ngenerated by a single generator, whereas it relies on 80-bit entropy in the use\ncases with distributed generators. SCRU128 fills the 80-bit field with a random\nnumber when a new ID is infrequently (less than one ID per second) generated.\nFor the distributed high-load use cases, SCRU128 assigns different lifetimes to\nthe three entropy components to improve the collision resistance:\n\n1.  24-bit `counter_hi`: reset to a random number every second\n2.  24-bit `counter_lo`: reset to a random number every millisecond\n3.  32-bit `entropy`: reset to a random number for every new ID generated\n\nThe longer lifetimes of `counter_hi` and `counter_lo` reduce the number of\nrandom numbers consumed and accordingly reduce the probability of at least one\ncollision because, for a given length of random bits, the less the number of\ndice throws, the lower the chance of collision.\n\nIn other words, generators are assigned to 24-bit `counter_hi` buckets every\nsecond, and thus they will not collide with each other as long as their buckets\ndiffer, even if each generates a bunch of IDs. 24-bit random numbers usually\ncollide if millions of instances are generated, but the one-second interval of\n`counter_hi` renewals decreases the number of trials drastically. Nevertheless,\n`counter_hi` is refreshed every second to prevent potential attackers from\nexploiting this field as a generator's fingerprint.\n\nEven within the same bucket, the generators will not collide as long as initial\n`counter_lo` values are sufficiently distant from each other. Such a near match\nprobability, if tried only once a millisecond, is much lower than [the simple\nbirthday collision probability] calculated over all the IDs generated within a\nmillisecond. `entropy` provides additional protection in the extremely rare\ncases where both `counter_hi` and `counter_lo` collide, but it is primarily\nintended to ensure a certain level of unguessability of consecutive IDs\ngenerated by a single generator.\n\n[the simple birthday collision probability]: https://en.wikipedia.org/wiki/Birthday_problem\n\n### License\n\nThis work is licensed under a [Creative Commons Attribution 4.0 International\n(CC BY 4.0) License].\n\n[Creative Commons Attribution 4.0 International (CC BY 4.0) License]: http://creativecommons.org/licenses/by/4.0/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fscru128%2Fspec","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fscru128%2Fspec","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fscru128%2Fspec/lists"}