A percentage rollout has one hard requirement people discover late: it has to agree with itself. If your Go backend puts user u_1732 in the 10% that sees the new checkout, the browser SDK rendering that same user had better agree, or the user gets the new API with the old UI. Random sampling per request fails this instantly. So does anything that depends on language-specific hashing.
feat’s answer is a single bucketing recipe that every SDK reproduces exactly. Here it is, in full:
input = salt + "." + flagKey + "." + contextKey
hash = SHA-1(input)
n = first 8 bytes of hash, read as a big-endian integer
bucket = (n >> 4) mod 100000
The bucket is an integer from 0 to 99,999. Rollout weights are integers that sum to 100,000 (so 0.001% resolution), and the SDK walks the cumulative weights until it passes the bucket value. That variation wins. There’s no state to store and nothing to coordinate: any process that knows the flag configuration and the user’s key computes the same answer.
A few of these choices deserve explanation.
Why SHA-1? Not for security; nothing here is secret. We need a function that’s uniform, fast, stable forever, and available in every runtime we ship: crypto.subtle in browsers and workers, crypto/sha1 in Go, hashlib in Python, digest in Ruby. SHA-1 is the intersection of “everywhere” and “good enough distribution,” and since collisions don’t matter for bucketing, its cryptographic retirement is irrelevant.
Why the 4-bit shift? Bucketing recipes in this industry are usually written as “take the first 15 hex characters of the hash, then mod by 100,000.” Fifteen hex digits are exactly 60 bits, so reading the first 8 bytes and shifting off the low 4 computes the same integer without a detour through hex strings. The shift keeps us compatible with that convention; each port then does the math in whatever exact-integer type its language offers (BigInt in JavaScript, ordinary integers in Python and Ruby, uint64 in Go). Each port’s source carries a comment pinning it to this exact recipe.
Why a per-flag salt? Without it, the same user would land at the same percentile for every flag: whoever falls in the first 10% would be the guinea pig for every rollout you ever run, and your early-adopter cohort would quietly become a fixed set of people. Each flag gets a random 16-hex-character salt at creation, so rollout populations are independent across flags but stable within one.
What’s the contextKey? Each rollout declares which context kind it buckets by, usually user, sometimes organization when you want a whole tenant to flip together. The key of that context is what gets hashed. If an evaluation doesn’t carry that context kind at all, the rule declines to bucket and evaluation falls through to the next rule, rather than inventing a bucket from a missing value.
The full decision order (individual targets, then rules, then the default) is documented in the evaluation reference, and the wire format the SDKs consume is public as @feathq/datafile-schema. If you port the recipe above and find an input where your numbers disagree with ours, that’s a bug: tell us.