When I wrote the Elo matchmaking post, the part that kept nagging me was the K-factor. Elo stores one number per player and quietly assumes it is equally trustworthy for everyone — the account with 4,000 games and the one that signed up this morning move by exactly the same amount. Glicko-2 is the standard answer to that problem: it keeps a rating, a measure of how sure the system is about that rating, and a measure of how erratic the player has been. This is a plain-English walk through the three numbers, the eight steps, the one mistake almost every implementation makes, and the honest test for whether you need it at all.
The three numbers
- Rating (r) — the familiar skill estimate, on the same 1500-centred scale as Elo. New players start at 1500.
- Rating deviation (RD) — the standard deviation of that estimate. New players start at 350. A player at 1850 with RD 50 has a 95% interval of roughly 1750–1950; the same rating with RD 300 means the system barely knows anything.
- Volatility (σ) — how much the rating is expected to fluctuate, default 0.06. It rises when someone produces results that don't fit their rating (a long stable run followed by a string of upsets) and falls when they perform consistently.
RD is the one that does most of the work. It controls the step size: high RD means "we are guessing, move a lot"; low RD means "we're confident, move a little". That single idea removes the need for the hand-tuned K-factor tiers most Elo deployments end up with.
The algorithm, step by step
Glickman's paper lays it out in eight steps. Stripped of notation, they are:
- Initialise. Unrated player → r = 1500, RD = 350, σ = 0.06. Choose the system constant τ once, up front.
- Convert to the Glicko-2 scale.
µ = (r − 1500) / 173.7178andφ = RD / 173.7178. The internal maths runs on this scale and converts back at the end; mixing the two scales is the classic source of nonsense output. - Estimated variance v. Sum over the period's opponents of
g(φ_j)² · E · (1 − E), whereEis the expected score against that opponent andg()shrinks the influence of opponents whose own RD is large. - Improvement Δ.
v · Σ g(φ_j)(s_j − E)— how far the actual results pulled away from the expected ones. - New volatility σ'. Solved numerically (Illinois-algorithm bisection) from Δ, φ, v and τ. This is the only part with a loop in it.
- Pre-period deviation φ*.
√(φ² + σ'²)— uncertainty grows first, then the results shrink it. - Update.
φ' = 1 / √(1/φ*² + 1/v)andµ' = µ + φ'² · Σ g(φ_j)(s_j − E). - Convert back.
r' = 173.7178 · µ' + 1500,RD' = 173.7178 · φ'.
Glickman's own worked example is the best unit test you will find: a player at 1500 / RD 200 / σ 0.06 plays three opponents in one period — 1400 (RD 30, win), 1550 (RD 100, loss) and 1700 (RD 300, loss). The result is r ≈ 1464.06, RD ≈ 151.52, σ ≈ 0.05999. If your implementation reproduces those three numbers to two decimals, the maths is right. If it doesn't, the bug is almost always in step 2 or step 5.
The mistake nearly everyone makes: rating periods
Glicko-2 is not an online, per-match algorithm. It treats every game inside a
rating period as if it happened simultaneously, and Glickman is explicit that the
system works best when players average at least 10–15 games per period. Most implementations
in the wild call updateRatings() after each match, which is exactly the usage the
npm glicko2 README warns against.
What happens if you do it anyway? Ratings get noisier, RD collapses faster than the evidence justifies, and volatility — which is trying to estimate the spread of performances across a batch — becomes close to meaningless. In practice you pick the shortest period that still batches several games:
- Busy ranked ladder: hourly or daily batches, run as a scheduled job.
- Small game with a few hundred players: weekly.
- If you truly need an instant number on screen, show a provisional Elo-style value between batches and reconcile it to the Glicko-2 rating at the end of the period.
Tuning τ
τ constrains how much volatility can move in one period. Reasonable values are 0.3–1.2, with 0.5 the common default. Smaller values (0.2–0.3) prevent freak results from causing enormous rating swings — worth it in any game with a large luck component, which includes almost everything with cards, dice or spawn randomness. There is no theoretically correct value: Glickman's advice is to test it against your own data and keep whichever maximises predictive accuracy. That is a genuinely easy experiment — replay six months of match history at τ ∈ {0.2, 0.4, 0.6, 0.8, 1.0}, score each run by log-loss against actual outcomes, pick the winner.
Inactivity, and why it matters more than you think
Between periods a player who did not play gets φ ← √(φ² + σ²): rating unchanged,
uncertainty larger. This is the feature Elo simply cannot express, and it solves two real
problems at once. Returning players are re-measured quickly instead of grinding back over
fifty games, and rating-parking — climbing to a peak and then hiding to protect a leaderboard
spot — stops working, because the leaderboard should rank by a conservative estimate, not raw
rating.
Use r − 2·RD for public leaderboards. Someone at 2100 with RD 40 outranks someone
at 2200 with RD 250, which is correct: the system is 95% confident about the first player and
openly guessing about the second. Keep raw r for matchmaking, where you want the
best point estimate, and keep RD as an input to how wide you're willing to search for
opponents.
So should you upgrade from Elo?
Honest answer: only if at least two of these are true for you.
- You have a meaningful stream of new players and their ratings take too long to settle.
- Players go inactive for weeks and come back mismatched.
- You already maintain multiple K-factor tiers (provisional, established, high-rated) — that is a hand-rolled, worse version of RD.
- You want honest confidence intervals for leaderboards, placement, or "this match is a fair fight" checks.
If none apply — a casual game, a leaderboard nobody optimises against — Elo with a sane
K-factor is fine, and the operational cost of batched rating periods is real. Also note the
scope limit: Glicko-2 models one-on-one games. For team matches the usual choices are
TrueSkill or the open-source openskill family, which handle per-player
contribution and free-for-all placements natively.
Implementation notes
- Don't write step 5 yourself the first time. The
glicko2npm package and the TypeScriptglicko2.tsport both implement the Illinois iteration correctly; port only after you have a passing test against the paper's example. - Store all three values per player, plus the timestamp of the last period they played in — you need it to apply inactivity inflation for skipped periods.
- Cap RD at 350 on the way up. Uncompressed inflation over years of inactivity can otherwise push it past the initial uncertainty, which is nonsense.
- Persist rating history per period. When something looks wrong six months later, the batch log is the only way to tell a bug from a genuinely strange run of results.
Elo's brilliance was fitting a whole rating system into one number in an era of paper records. Glicko-2's brilliance is admitting that one number was always a summary of a distribution — and that if you keep the spread around, most of the awkward patches Elo deployments accumulate just stop being necessary.