Why 'Random' in Your Software Might Not Be Random Enough
Password reset links, discount codes and raffle draws all depend on randomness. Weak randomness in any of them can cost you money or data. Here's what to check.
A client once asked me to look at why their referral code system kept handing out codes that didn't feel random at all. People were noticing patterns. A few customers had worked out they could guess other people's codes. The culprit wasn't a hacker. It was a six-character code generated from a weak source of randomness that just wasn't spreading values evenly across the possible range.
Randomness turns up in more of your software than you'd think: password reset links, discount codes, referral codes, API keys, the order items get shuffled in, who wins a prize draw. Most of the time it works fine. But "looks random" and "is random enough for the job" are different things, and the gap between them only shows up once something goes wrong.
Where weak randomness actually bites you
The risk depends entirely on what the randomness is protecting. A shuffled list of blog posts on your homepage doesn't need to be unguessable. A password reset token does, because if someone can predict or narrow down the next one, they can take over an account without ever touching a password.
The same logic applies to anything that acts like a secret: API keys, invite links, one-time codes sent by SMS, signed URLs for downloading a file. If the value is short, predictable, or generated from something an attacker can observe (like the time the request was made), it stops being a secret and starts being a guess with good odds.
Discount and referral codes sit in a middle ground. Nobody's account gets compromised if someone guesses a 10% off code early, but it still costs you money, and if the pattern is obvious it can spread fast on social media or deal forums.
The functions that look random but aren't
Most programming languages have two tiers of randomness, and the names don't make the difference obvious. PHP's rand() and mt_rand(), for example, are fine for picking a random background colour or shuffling a quiz question order. They're not built to resist someone trying to predict the output, because their internal state can, in some cases, be reconstructed from a handful of observed values.
Then there's a second tier, usually called cryptographically secure randomness, like PHP's random_bytes() or random_int(). These are slower and slightly more awkward to use, which is exactly why they sometimes get skipped under deadline pressure. They're the ones that should sit behind anything resembling a secret: tokens, keys, codes tied to money or access.
I've also seen uniqid() used as if it generates a random ID. It doesn't, not really. It's based on the current time in microseconds, which means two values generated close together can be far more similar, and far more guessable, than the name suggests.
Short codes and the birthday problem
There's a separate issue that has nothing to do with the quality of the randomness: how many values you're actually picking from. A 6-digit numeric code sounds like a million possibilities, which feels safe until you remember that collisions happen much sooner than people expect. If you're generating thousands of codes a day from a small pool, you'll start hitting duplicates far earlier than intuition suggests. This is the same maths behind why two people in a room of 23 are more likely than not to share a birthday.
The fix isn't always "make the code longer", because a 12-character discount code is annoying to type into a checkout box. It's usually a combination of a decent-sized character set, a uniqueness check against the database before the code is issued, and a sensible retry if there's a clash. That's a small piece of logic, but it's the difference between a code system that quietly fails once a month and one that never does.
Raffle draws and fairness
If you're running a prize draw, a seat allocation, or any process where fairness is visible to the people involved, there's a third consideration: can you show your working? Picking a winner with an insecure or badly seeded random function might still be unbiased in practice, but if someone challenges the result, "trust me" isn't a great answer. For anything with a public or contractual stake, it's worth logging the seed, the pool of entries, and the method, so the draw can be reproduced and checked later if it's ever questioned.
What good randomness looks like in practice
None of this needs to be complicated. For most business software, the checklist is short:
- Anything that acts as a secret (tokens, keys, reset links) uses a cryptographically secure generator, not a general-purpose one.
- Short codes are checked for uniqueness against existing values before they're handed out, with a retry on collision.
- Codes and tokens are long enough, and drawn from a wide enough character set, for the volume you actually generate.
- Anything with a fairness claim attached to it (a draw, an allocation) can be explained and reproduced if someone asks.
It's the kind of detail that's invisible when it's done right and expensive when it isn't, which is exactly why it's worth getting right the first time rather than patching it after a customer notices. When this sort of feature is part of a wider build, it's one of the things I check for by default rather than something a client needs to think to ask about, whether that's inside a custom software project or a smaller piece bolted onto an existing system.
Next time you're reviewing a brief or a quote that mentions codes, tokens or a draw of any kind, it's worth asking the simple question: random enough for what, exactly?