🍱 Lunchbox Hands

secret-santa

Secret Santa Without a Hat: Derangements, Exclusions, Private Links

Drawing names from a hat fails about 63% of the time with 5+ people. The math of derangements, when couple exclusions make a draw impossible, and how reveal links work.

Everyone writes their name on a slip, the slips go in a hat, everyone draws one, and somebody unfolds their slip and finds their own name. Then you all put the slips back and start again. With five or more people, a plain hat draw gives someone their own name about 63% of the time. That isn’t bad luck. It’s a classic result in combinatorics, and the number barely changes no matter how big your group gets. The math also explains why couple exclusions can make a draw impossible, and why “keep redrawing until it works” and “everyone gives to the next person in a circle” give you different kinds of draws. This post covers all three, then explains exactly what our Secret Santa generator does, including what its private links actually contain.

Why the hat fails: derangements

A valid Secret Santa draw is a permutation in which nobody ends up with their own name. Mathematicians call that a derangement, and the number of derangements of n people is written D(n). It follows a short recurrence (OEIS A000166 lists it, along with the terms below):

D(n) = (n − 1) × (D(n − 1) + D(n − 2)),   D(0) = 1, D(1) = 0

A fair hat draw picks one of the n! possible permutations, each equally likely. So the chance that nobody draws themselves is D(n) / n!. Here are the numbers, computed from the recurrence:

PeopleValid draws D(n)All draws n!Hat worksSomeone draws themselves
32633.33%66.67%
492437.50%62.50%
54412036.67%63.33%
626572036.81%63.19%
71,8545,04036.79%63.21%
814,83340,32036.79%63.21%
9133,496362,88036.79%63.21%
101,334,9613,628,80036.79%63.21%

The “works” column settles almost immediately at 1/e ≈ 0.36788. OEIS gives the related identity D(n) = round(n!/e) for n ≥ 1, which is the same fact in another form. Adding people doesn’t help. A family of 6 and an office of 60 have essentially the same odds: roughly two out of three hat draws need a redo.

If your rule is “start over whenever anyone draws themselves,” the expected number of attempts is n!/D(n). That’s 24/9 ≈ 2.67 for four people, and it approaches e ≈ 2.72 as the group grows. Most groups need about three goes, and each failed go tells someone who they didn’t draw.

The put-it-back version is worse in a different way

Many groups use a gentler rule: if you draw yourself, put the slip back and draw again. That works until the last person reaches into the hat and finds only their own name. We simulated this rule 400,000 times per group size, and the last person got stuck about 13.9% of the time with 4 people, 11.3% with 6 people and 7.6% with 10 (an exact calculation gives 13.89%, 11.28% and 7.57%). The failure shows up at the very end, after everyone else already knows their person, so you have to redraw the whole thing anyway.

Exclusions: when the draw becomes impossible

Real groups add rules: partners shouldn’t draw each other, nor should people in the same household, nor last year’s pairs. Each rule removes options, and some combinations remove every option.

The smallest impossible case is three people where two are a couple. Alice and Bob are excluded from each other, and neither can draw themselves, so both must draw Carol. Only one of them can. No valid draw exists.

The general rule for households is a counting argument. Say a household of k people are all excluded from each other. Each of them has to give to someone outside the household, and no two can give to the same person. That needs at least k outsiders, so a valid draw requires:

household size k  ≤  everyone else (n − k)

Five people with a household of three fails (3 > 2). Six people with a household of three works (3 ≤ 3), but just barely. The household and the other three have to swap gifts across the divide, so nobody in either group gives to someone in their own group. Overlapping rules, like two households plus a “not last year’s person” list, are harder to check by hand. This is a perfect-matching problem, and it’s exactly the kind of thing a program should check for you.

Exclusions also make blind redrawing slower. We brute-forced every permutation for groups made entirely of couples, where nobody can draw themselves or their partner:

GroupAll permutationsValid drawsChance a random shuffle is validExpected redraws
2 couples (4 people)24416.67%6.00
3 couples (6 people)7208011.11%9.00
4 couples (8 people)40,3204,75211.79%8.48

A program that redraws blindly gets there eventually, but if the rules are impossible it loops forever, because it can’t tell “unlucky” from “impossible.”

Two ways to make a valid draw (and which one we use)

Rejection sampling: shuffle, check, and reshuffle on failure. If the shuffle is fair, every valid draw is equally likely. That’s the most defensible kind of fairness. The downside is the problem above: it can’t detect an impossible set of rules, so it needs an attempt cap and a vague “gave up” message.

The single circle: shuffle the names into a line and have each person give to the next one, with the last giving to the first. This always succeeds instantly and never produces a self-draw. But it only produces one kind of derangement, a single cycle through everyone. With four people, only 6 of the 9 valid draws are single cycles. The other 3 are two pairs swapping gifts (A↔B, C↔D), and the circle method can never produce those. As the group grows, single cycles become a smaller share of all valid draws (27% at 10 people, from (n − 1)! / D(n)). Exclusions also don’t fit neatly. You have to reshuffle the circle until no two excluded people are neighbours.

Our generator does neither. From the source (src/lib/everyday/secret-santa-generator.ts), it runs a randomized backtracking search:

  1. It builds an “allowed” table, where everyone is blocked from drawing themselves and every exclusion blocks both directions. If Alice can’t draw Bob, Bob can’t draw Alice.
  2. It runs two quick checks. Is there someone who has nobody left to draw? Is there someone nobody is allowed to draw? Either one gets a specific error naming that person.
  3. It assigns givers starting with the most constrained, breaking ties in random order. Each giver tries the remaining allowed receivers in shuffled order, and it backs up when it hits a dead end.
  4. If the search runs out of options, the rules are provably impossible, and you get “the exclusions leave no way to match everyone” instead of an endless spinner. As a safety valve, it stops after 200,000 search steps and tells you the rules are too restrictive to search. In our tests, even a 60-person group containing a 30-person household finished in a couple of milliseconds without coming close to that limit.

The draw accepts any valid derangement, not just single circles, so pairs who swap gifts can happen. We ran the generator a million times for four people and counted the outcomes. Each person drew each of the other three about 33.3% of the time. That’s what you’d expect, since with no exclusions the algorithm treats every name the same way. But the nine possible draws were not equally likely. Each of the six single-circle draws came up about 12.0% of the time, and each of the three swap-pairs draws about 9.2%, against 11.1% for a perfectly uniform draw. The gap shrinks as the group grows. At 8 people, some pair swaps with each other in 38.4% of our draws versus 39.4% for a uniform draw. So here’s the honest summary: you’re equally likely to get any of the other people, but our draw is not exactly uniform over every possible arrangement. No one at a gift exchange will notice, but we’d rather say so.

The randomness comes from crypto.getRandomValues, not Math.random(). The page turns one 32-bit value into a number in [0, 1) and scales it to the number of candidates. For a group of a few dozen people, the rounding bias from that mapping is far below one part in a million. Why we care about the source at all is covered in Is Math.random() random enough?.

The other half of Secret Santa is keeping the result secret, which is why most websites ask for everyone’s email address. Ours doesn’t. After the draw, the organizer gets one link per person, and each link carries that person’s result in the URL itself, after the #:

https://lunchboxhands.com/tools/secret-santa-generator#r=eyJnIjoiQWxpY2UiLCJyIjoiQm9iIn0

That string after r= is not encrypted. It’s Base64 (the URL-safe variant) of a small piece of JSON with short key names:

{"g":"Alice","r":"Bob"}

g is the giver and r is the receiver. If the organizer filled them in, b (budget), d (exchange date), n (note) and t (group name) come along too, which is why the note field is capped at 500 characters: it’s copied into every link. When the page opens, it decodes the fragment in your browser and shows “Hi Alice!” with a Reveal my person button. The receiver’s name isn’t put on the page until you press it. A short example like the one above even decodes in our Base64 decoder. Longer ones may contain - or _, which need swapping to + and / first.

Two facts make this work without a server:

  • Browsers don’t send the fragment to the server. MDN puts it plainly: “The fragment is not sent to the server when the URI is requested; it is processed by the client.” So when Alice opens her link, our server only sees a request for /tools/secret-santa-generator. The names stay in the address bar.
  • Nothing is stored anywhere. The names, exclusions and results never leave the organizer’s browser. The only custom analytics event the tool sends is draw with the participant count (we checked the component source), and our analytics setup reports the page address without the # fragment when a page loads, so opening a reveal link doesn’t send its names to Google Analytics either. That also means there’s no database to recover a lost link from. If a link is lost and the organizer has closed the page, the fix is to press Redraw and send everyone new links.

Now the limits, stated honestly:

  • The organizer can peek. They generate every link, so they could open any of them. Our tool says this on the results screen: the draw is “private by courtesy, not cryptography.” What the results screen does do is show only giver names and shortened links, never receivers, including the organizer’s own. If the organizer wants a real surprise, they can copy their own link without opening it and hand the rest to someone else to send.
  • Anyone who sees the full link can decode it. That includes whatever chat app carries the message and anyone who reads it over the recipient’s shoulder. Send each link in a private message, never in the group chat.
  • Old links keep working. A redraw creates new links but can’t cancel old ones, since nothing on a server knows they exist. If you redraw, make sure everyone uses the new set.

A system with real cryptography, where even the organizer can’t see the results, needs either a server that holds a secret or a more involved protocol between participants. For a family gift exchange, we decided that “nobody has to sign up, and nothing gets stored” was worth more than defending against an organizer who wants to cheat.

Running your draw

If you’re planning an exchange for this season, here’s the short version:

  1. Paste the group into the Secret Santa generator, one name per line. Names that differ only in capitalization or in spaces at the start or end are merged into one person, so add a last initial to tell two Sams apart.
  2. Add a “Don’t match” row for each couple, sibling pair or last year’s match. For a whole household, add a row for every pair within it. Remember the rule: no household can be more than half the group.
  3. Set the budget, exchange date and a note if you want. They go into every link.
  4. Press Draw names, then use Copy all messages and send each message privately. Each recipient opens it, makes sure nobody’s watching, and presses Reveal.

If you only need to pick one winner rather than run a full exchange, the wheel spinner or the random number generator is the simpler tool. And the countdown timer will tell everyone how many shopping days are left.