Almost every competitive Discord community starts in a spreadsheet. Someone opens Google Sheets, adds a Wins column and a Losses column, and it works. That is not a mistake to be corrected — it is the correct first move. A spreadsheet costs nothing, needs no permissions, and can be reshaped in ten seconds when you realise you're tracking the wrong thing.
The useful question isn't which tool is better. It's when the sheet stops paying for itself, and that moment is more specific than "when you get big". Plenty of 200-player leagues run fine on a sheet. Plenty of 25-player leagues have already outgrown one.
This post is the honest version: a genuinely working Elo spreadsheet you can build today, the structural reason it gets uncomfortable, and the four things that actually force a change.
Where a Spreadsheet Never Breaks
Start with the case for staying. If your league is tracking wins, losses and points, a spreadsheet will serve you indefinitely and you should stop reading here.
The reason is worth stating precisely, because it's the same reason the other case falls apart. A league table is a map operation: each row is independent, and every cell in the standings is a COUNTIF or a SUM over rows that don't care about each other. Add a match at the bottom, and nothing above it changes. Sort by points, filter to one team, insert a forgotten fixture in the middle — the sheet doesn't care. Every one of those operations is safe, because there is no state carried between rows.
That's a genuinely good fit, and it's why round-robin formats — EA FC seasons, fixture-based leagues, anything with a table rather than a ladder — survive in Sheets far longer than ladder leagues do. Spreadsheets are extremely good at independent rows.
Elo is not independent rows.
Elo in a Spreadsheet, Done Properly
Here's a correct implementation, so we're arguing about a real thing rather than a strawman.
One row per match. Columns:
| A | B | C | D | E | F | G | H | |
|---|---|---|---|---|---|---|---|---|
| 1 | Date | Player A | Player B | Score A | A before | B before | A after | B after |
Score A is 1 for a win, 0.5 for a draw, 0 for a loss. Put your K-factor in K1.
The expected score is the standard logistic:
=1/(1+10^((opponent_rating - player_rating)/400))
And the update is new = old + K × (actual − expected). That part is easy. The hard part is A before and B before, because a player's rating going into a match is the accumulated result of every match they've played before it. In row 3:
E3: =IFERROR(LOOKUP(2, 1/(($B$2:B2=B3)+($C$2:C2=B3)),
IF($B$2:B2=B3, $G$2:G2, $H$2:H2)), 1000)
F3: =IFERROR(LOOKUP(2, 1/(($B$2:B2=C3)+($C$2:C2=C3)),
IF($B$2:B2=C3, $G$2:G2, $H$2:H2)), 1000)
G3: =E3 + $K$1*(D3 - 1/(1+10^((F3-E3)/400)))
H3: =F3 + $K$1*((1-D3) - 1/(1+10^((E3-F3)/400)))
That works. LOOKUP(2, 1/(condition), values) is the standard idiom for "the last row above me where this was true" — the 1/ turns non-matches into errors that LOOKUP skips. The IF picks the right rating-after column depending on which side the player was on, and IFERROR supplies the 1000 starting rating for a player's first appearance. Fill it down and you have a real Elo ladder.
Nothing about that is beyond a competent spreadsheet user, and if you want to sanity-check the numbers it produces, the Elo calculator runs the same core formula with the extra knobs — rating scale, curve, and the caps on lopsided matchups — exposed alongside it.
But look at what those formulas are doing.
The Structural Problem: Elo Is a Fold
Every cell in that sheet scans every row above it. That's not a stylistic choice — it's forced. A rating is a running total, so match 400 depends on match 399, which depends on match 398, all the way back.
In functional terms, a league table is a map and Elo is a fold. The practical consequence is a test you can apply to any spreadsheet in ten seconds: if you can't write it as an ARRAYFORMULA, it's a fold. You cannot write Elo as an ARRAYFORMULA, because each output is an input to the next one. That is the whole difference, and everything below follows from it.
Three things follow immediately:
It gets slow, quadratically. Every row scans the rows above it, so recalculation cost grows with the square of your match count. At 200 matches you won't notice. At 2,000 you'll watch the sheet think after every edit, and at 5,000 people start keeping it closed.
You can never sort or filter it. The chronological order is the data. Those relative ranges ($B$2:B2) mean "everything above me" — sort by rating to see the standings and you have shuffled the timeline, and the ratings recompute against a history that never happened. Insert a forgotten match into the middle and every rating below it silently changes. Both of these are things people do to spreadsheets reflexively, without thinking, because on every other sheet they've ever used they were safe.
One typo rewrites everything after it. Enter a result backwards in row 12 of 400 and the sheet doesn't complain — it recalculates, correctly, and hands you 388 rows of new ratings. That's the right behaviour, and it's also why it's dangerous: the error and the correction are both invisible.
None of this is a reason to panic at match 30. It is the reason a sheet that felt effortless in month one feels fragile in month six, and it's useful to know that the fragility is structural rather than something you did wrong.
The Four Things That Actually Force the Switch
Performance isn't usually what breaks the camel's back. These are.
1. Corrections have no memory
Someone reports a match backwards. You find it three weeks later and fix it. The sheet does exactly what it should — and eleven people's ratings change overnight with no record of why. There's no "this match was corrected on the 14th" anywhere, because the sheet only stores the current state, not the sequence of decisions that produced it. Version history technically has it, but version history is a forensic tool nobody reads, not an explanation you can point a confused player at.
This is the failure that costs you trust rather than time, and it's the one leagues consistently underestimate. A rating that changes without an explanation is indistinguishable, from the player's side, from a rating that was tampered with.
2. Edit access is all-or-nothing
To let someone record results, you give them edit access to the sheet. Edit access to the sheet is edit access to every rating in it. There is no way to express "can add a match, cannot change a number", which is precisely the permission you want for the six people helping you run the league. You end up either bottlenecking every result through one person or extending full write access to your rating data on the strength of trust alone.
Both work fine until they don't, and the failure mode of the second one is very unpleasant.
3. The reporting bottleneck creates the disputes
When results go into a sheet, they go through a human. Matches finish at 11pm, the person with the sheet enters them the next afternoon, and the gap between "we played" and "it's recorded" is where every argument lives. Scores get misremembered, one side reports and the other doesn't, and the person entering data is now adjudicating a dispute they didn't witness.
Compare that with results being submitted by the players, at the moment of the match, with both sides seeing what was entered. Most "our league has a disputes problem" is really a reporting-latency problem wearing a disguise. The rules that hold up matter, but they matter much less when the gap is thirty seconds instead of a day.
4. Nobody looks at a link
This is the quiet one. Players want to see their rank where they already are, which is Discord. A spreadsheet link gets clicked enthusiastically for two weeks and then basically never. When the standings become invisible, the ladder stops being a reason to queue — and a ladder nobody checks is a ladder that quietly dies. An auto-updating leaderboard message in the channel gets read every time someone scrolls past it, which turns out to matter more for retention than any amount of rating accuracy.
So When, Concretely?
Not a player count. The real trigger is how many people need to write to it.
One person recording everything, at a known time, for a group that trusts them: a spreadsheet is fine, potentially forever. It's a single-writer system and single-writer systems are exactly what spreadsheets are good at.
The moment results arrive from several people, at unpredictable times, faster than one person can keep up — that's when the sheet becomes the constraint. In practice that tends to land somewhere around two or three sessions a week with more than one group playing, but the count matters less than the shape.
Five questions. If you're answering yes to three or more, the sheet is costing you more than it's saving:
- Is there a person whose absence stops results being recorded?
- Has a correction ever changed ratings without the affected players being told why?
- Do you have people you'd trust to enter results but not to edit ratings?
- Do you find yourself avoiding sorting or filtering the sheet because you know it'll break?
- When someone asks what rank they are, is the honest answer "let me check"?
The Honest Cost of Switching
Two things are worth knowing before you move, because neither is usually mentioned.
You cannot upload your spreadsheet. Team Up's import is a backup-and-restore format — a JSON file produced by /backup export, designed for moving a league between servers or recovering from a mistake. It is not a CSV importer, and no ranking bot's import will reconstruct match history from an arbitrary sheet, because your sheet's columns are yours. There are two real options: start everyone at the default rating and let a few weeks of matches sort the order out, or seed the ratings you already have with /set_rating and carry on from there.
Most leagues should just start fresh. That sounds like the lazy advice, so here's the argument: a rating produced by a hand-maintained sheet has accumulated every typo, every retroactive edit, and every K-factor you changed your mind about, with no record of any of it. Four weeks of correctly computed matches will usually describe your players better than the number they finished the sheet with. Seeding is worth it when your ladder is long-established and the ordering is genuinely well-earned — and even then, seed the ordering, not the exact values.
Your community has to learn a new habit, and that costs more than the setup does. People who currently ping a person now run a command. Budget a couple of weeks of reminders, and set the reporting flow up before you announce it rather than after.
What you get in exchange is roughly the inverse of the four failures: results entered by the people who played, ratings that only the bot can write, an audit trail for every correction, and standings that live in the channel instead of behind a link. The leaderboard setup guide covers the mechanics; K-factor curves is worth reading first if you're choosing a rating pace, since it's the one setting that's annoying to change later.
Frequently Asked Questions
Is a spreadsheet ever better than a bot?
Yes, in two cases. If you're tracking a fixture-based league table rather than a ladder, a spreadsheet handles it natively and forever — independent rows are what it's built for. And if you want a rating rule nobody implements, a sheet lets you write whatever formula you want, which no bot will.
Can I keep the spreadsheet as a backup?
You can, but it stops being a backup the moment the two disagree, and they will. Better to use the bot's own export as your backup — it round-trips, which a sheet reconstruction doesn't. Keep the old sheet read-only as an archive of the era it covered.
How many matches can a spreadsheet Elo handle?
The formulas above are O(n²), so recalculation slows noticeably somewhere in the low thousands of rows and becomes unpleasant beyond that. You can push it much further with a script that recomputes on demand instead of live formulas — but at that point you've written a ranking bot with worse permissions.
Will I lose my match history if I switch?
The matches you recorded in the sheet stay in the sheet — nothing imports them, so keep it archived. Ratings can be carried across with /set_rating, but the history behind them can't be reconstructed. This is the strongest argument for switching earlier rather than later: the history you lose is only ever the history accumulated up to the day you move.
Do I need a bot to run an Elo ladder at all?
No. The formulas in this post are complete and correct, and a small league run carefully in Sheets is a perfectly legitimate competitive ladder. What a bot buys you is not better math — it's the same math with write permissions, an audit trail, and results entered by the people who played.
Outgrown the sheet? Team Up records matches from Discord, computes ratings with configurable K-factor and rating scales, keeps an audit trail of every correction, and posts auto-updating leaderboards in your channels. If you're still comparing options, the ranking bot comparison covers the alternatives honestly.
