Home › Multiplayer & game networking › Rating & matchmaking
algorithm low

Elo rating

Problem You have many competitors and only the results of head-to-head comparisons, and you need one comparable strength number each, kept current as results arrive, to rank, forecast or pair them.

One number per competitor, moved by K × (actual − expected score) after each pairwise result; expected score is logistic in the rating difference.

O(1) per game update; O(games) per rating period
ratingelopairwise-comparisonskillrankingmatchmakingleaderboardbradley-terrylogistic
Overview

Elo models each competitor's performance as noisy around a hidden strength, so the win probability depends only on the rating difference. After each pairwise result both ratings move toward it in proportion to the surprise: upsets gain a lot, expected wins little, and the two changes cancel out. It is the online form of the Bradley-Terry model of paired comparisons. Used for chess federations, online matchmaking, sports forecasts and AI-model leaderboards.

Key facts

Use when

  • Chess and other two-sided games: stable populations playing many games (FIDE, leagues).
  • Sports power ratings and forecasts: teams meeting repeatedly (football, basketball).
  • Online ladders and leaderboards, including head-to-head votes between AI models.
  • You need a rating people can recompute by hand and understand.

Avoid when

  • Online games with many new or returning players: slow convergence and no confidence for matchmaking; use Glicko-2 or TrueSkill.
  • Team or multiplayer free-for-all games: Elo is pairwise; use TrueSkill/OpenSkill-style models.
  • A fixed set of comparisons collected offline (e.g. model evaluations): fit Bradley-Terry in batch instead of updating online; it is order-independent and gives confidence intervals.

Deep dive

How it works

In: two ratings and a result (1 / 0.5 / 0) · Out: two updated ratings · State: one rating per competitor (plus a game count for K) · Cost: O(1) per result.

  1. Model: performance difference is logistic (most systems) or normal (Elo's original, FIDE's table) with scale chosen so 400 points ≈ 10:1 odds.
  2. Expected score from the difference D = R_A − R_B: E = 1/(1 + 10^(−D/400)). FIDE caps |D| at 400 for the calculation.
  3. Rating period: sum K(S − E) over all games in the period using pre-period ratings (FIDE: monthly), or update after every game (online).
  4. K-factor is the step size: larger K converges faster but is noisier. Typical: 32-40 for new players, 10-20 for established ones.
  5. Initial rating: provisional rating from the first n games (performance rating), then switch to K updates.
P(win) for A
1.0 |                         ______
    |                   ___---
0.5 |-------------__--‾‾--------------   E = 1 / (1 + 10^(-D/400))
    |       ___---
0.0 |___---‾__________________________ D
      -800   -400    0    400    800

Code

/// Logistic Elo expected score for A against B.
pub fn expected(ra: f64, rb: f64) -> f64 { 1.0 / (1.0 + 10f64.powf((rb - ra) / 400.0)) }

/// FIDE-style K factor (rating regulations, simplified).
pub fn fide_k(rating: f64, games_played: u32, age: u32, ever_reached_2400: bool) -> f64 {
    if games_played < 30 || (age < 18 && rating < 2300.0) { 40.0 }
    else if ever_reached_2400 { 10.0 }
    else if rating < 2400.0 { 20.0 }
    else { 10.0 }
}

/// One rating period: all games use the pre-period rating; difference capped at 400.
pub fn period_update(r: f64, k: f64, games: &[(f64, f64)]) -> f64 {
    // FIDE: if K x games > 700, use the largest K with K x n <= 700.
    let k = if k * games.len() as f64 > 700.0 { (700.0 / games.len() as f64).floor() } else { k };
    let delta: f64 = games.iter()
        .map(|&(opp, score)| {
            let d = (r - opp).clamp(-400.0, 400.0);
            score - 1.0 / (1.0 + 10f64.powf(-d / 400.0))
        })
        .sum();
    r + k * delta
}

/// Per-game update for both players (online style). Returns new (ra, rb).
pub fn update(ra: f64, rb: f64, score_a: f64, ka: f64, kb: f64) -> (f64, f64) {
    let ea = expected(ra, rb);
    (ra + ka * (score_a - ea), rb + kb * ((1.0 - score_a) - (1.0 - ea)))
}

/// Performance rating of a result set (logistic inverse); used for provisional ratings.
pub fn performance(games: &[(f64, f64)]) -> f64 {
    let n = games.len() as f64;
    let avg_opp = games.iter().map(|g| g.0).sum::<f64>() / n;
    let p = (games.iter().map(|g| g.1).sum::<f64>() / n).clamp(0.01, 0.99); // avoid ±infinity
    avg_opp + 400.0 * (p / (1.0 - p)).log10()
}

#[test]
fn examples() {
    assert!((expected(1700.0, 1500.0) - 0.7597).abs() < 1e-4);
    let (a, b) = update(1500.0, 1700.0, 1.0, 20.0, 20.0); // upset win
    assert!((a - 1515.19).abs() < 0.01 && (b - 1684.81).abs() < 0.01);
    // Capped difference: a 2800 beating a 2000 is scored as if the gap were 400.
    let r = period_update(2800.0, 10.0, &[(2000.0, 1.0)]);
    assert!((r - 2800.91).abs() < 0.01);
    assert!((performance(&[(2000.0, 0.5), (2200.0, 0.5)]) - 2100.0).abs() < 1e-9);
}

Complexity and constants

  • O(1) per game; a rating period is a sum over the player's games.
  • Logistic vs normal: the logistic curve has fatter tails, i.e. gives underdogs more chances; FIDE still uses a normal-based lookup table (σ of a single performance = 200, so σ of a difference ≈ 283).
  • Convergence: with K = 20 a player 200 points underrated, scoring as expected for their true strength, gains about 20 × (0.76 − 0.5) ≈ 5 points per game against equals: dozens of games to converge. K = 40 halves that.
  • Rating noise at equilibrium: a player of constant strength random-walks around it with standard deviation ≈ √(87·K) points (no draws): about 30 at K = 10, 42 at K = 20, 59 at K = 40. Draws reduce it.

Efficiency

Cost: one update is two powf/exp calls and a few multiplies: order of tens of ns on a modern core, compute-bound but negligible; storage is one f64 (8 bytes) per competitor plus a counter. Real systems are bound by reading the game stream, not by the arithmetic.

Levers:

  • Fit Bradley-Terry in batch by maximum likelihood — order-independent, gives confidence intervals (LMSYS 2023 leaderboard post) — when the comparison set is fixed or replayed, as for an AI-model leaderboard.
  • Rating periods — sum K(S − E) over a period's games with pre-period ratings (FIDE Rating Regulations) — when updates arrive in bursts and order must not matter.
  • Table lookup of expected score — FIDE publishes a difference-to-score table (FIDE Rating Regulations) — when hand computation or exact reproducibility matters.
  • Parallel batches — ratings of disjoint pairs update independently (order of) — when replaying millions of historic games.

Naive → standard → state of the art:

  • Recompute all ratings from scratch after every game · O(games) per result
  • Online Elo update · O(1) per result, 8 bytes per competitor
  • Batch Bradley-Terry fit with bootstrap intervals (Chatbot Arena, arena-rank) · iterative optimisation over all comparisons, run offline

Implementation notes

  • Store ratings as floats (or tenths), round only for display; FIDE rounds per-game changes, accumulated rounding shifts long-run averages.
  • Use the pre-period rating for every game in a period, otherwise results depend on game order.
  • Draws matter: S = 0.5. In chess at master level, a large share of games are draws, and AI-model votes can end in ties; a model that predicts draw probability separately (e.g. Davidson extension of Bradley-Terry) helps forecasting but is not Elo.
  • Inflation/deflation: new players entering above their strength and leaving after losing donates points; control the entry rating and K for new players. FIDE's 2024 changes (floor 1400, one-time adjustment for players under 2000) were a response to deflation at the bottom.
  • For matchmaking, pair by rating difference but remember Elo has no uncertainty; give new players a provisional flag and higher K.

Pitfalls

  • Using one K for everyone online: newcomers take forever to reach their level and distort everyone they play on the way.
  • Comparing ratings across pools (FIDE vs USCF vs Lichess): each pool has its own offset and spread.
  • Allowing rating manipulation: losing on purpose to lower your rating for easier matches or prize sections; detect with performance anomalies.
  • Expected-score formula with natural log scale by mistake (exp(-D/400)): changes the scale by a factor ln 10.
  • Uncapped difference with tiny K at high ratings: a single loss to a much weaker player can wipe out a month of gains (FIDE's 400-point cap limits it).

Variants and alternatives

Glicko-2rating deviation and volatility; faster convergence for new or inactive players; Lichess.
Glickodeviation only; Chess.com.
TrueSkill / TrueSkill 2Bayesian, teams and free-for-all; Xbox Live.
Bradley-Terry fitted by maximum likelihoodthe model Elo updates online; batch, order-independent, with confidence intervals; LMSYS Chatbot Arena moved from online Elo to it in December 2023. Also good for historical ratings (e.g. engine rating lists with BayesElo/Ordo).
Elo with margin of victorysports models scale K by score margin.

In production

  • Chess: FIDE ratings (Elo with K 10/20/40); Lichess uses Glicko-2; USCF uses a logistic variant with variable K.
  • Esports and online matchmaking: Microsoft's TrueSkill, built for Xbox Live, extends the idea to teams with uncertainty.
  • Sports: FIFA's men's World Ranking has been Elo-based since 2018 ("SUM"); World Football Elo Ratings (eloratings.net); FiveThirtyEight sports forecasts (unlinked, not re-verified).
  • AI-model leaderboards: LMSYS Chatbot Arena ranks models from human pairwise votes, first with online Elo, now Bradley-Terry.

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
skillratingsRustatomflunder/skillratingscargo add skillratings————Elo, Glicko-2, TrueSkill and more; most used Rust option
arena-rankPythonlmarena/arena-rankpip install arena-rank————Bradley-Terry with confidence intervals; the LMArena leaderboard method
openskillPythonvivekjoshy/openskill.pypip install openskill————Multiplayer and team ratings (Weng-Lin), TrueSkill alternative

Sources

  • Elo, The Rating of Chessplayers, Past and Present, Arco, 1978.
  • FIDE, FIDE Rating Regulations (Handbook B.02), fide.com. handbook
  • Bradley, Terry, Rank Analysis of Incomplete Block Designs: I. The Method of Paired Comparisons, Biometrika, 1952. paper
  • Glickman, A Comprehensive Guide to Chess Ratings, American Chess Journal, 1995.
  • Glickman, Example of the Glicko-2 system. pdf
  • Herbrich, Minka, Graepel, TrueSkill: A Bayesian Skill Rating System, NIPS 2006. paper · project
  • Chiang et al., Chatbot Arena: An Open Platform for Evaluating LLMs by Human Preference, 2024. arXiv
  • Chiang et al., Chatbot Arena: New models & Elo system update, LMSYS blog, 2023. post
  • FIFA, Men's World Ranking procedure. page
  • Lichess, Rating systems. page

Used by

Systems (9):

Alternatives and related