Real-Time Matchmaking Data Pipelines with RisingWave

Real-Time Matchmaking Data Pipelines with RisingWave

Real-time matchmaking data pipelines built on RisingWave maintain continuously updated player skill ratings, win rates, and behavioral scores by processing game result events the moment they land. Matchmaking algorithms query these materialized views over a PostgreSQL interface to find fair matches in milliseconds — without batch jobs or stale player statistics.

Why Matchmaking Data Needs to Be Live

Skill-based matchmaking (SBMM) is only as good as the data feeding it. A rating system that updates daily means a player who just went on a 20-game winning streak still gets matched against opponents calibrated to their performance from yesterday. The result is lopsided matches, frustrated players, and increased churn.

Modern competitive games run thousands of matches per minute. Player performance evolves continuously: someone masters a new character, recovers from a bad week, or grinds their way to a new skill tier in a single session. The matchmaking data pipeline must reflect those changes in near-real-time to keep matches fair and engaging.

RisingWave solves this by maintaining incrementally updated player performance materialized views that are always current. When the matchmaking service queries for suitable opponents, it reads from views that already reflect the most recent completed matches.

Ingesting Match Results

Connect RisingWave to the Kafka topic that receives match result events from game servers:

CREATE SOURCE match_results (
    match_id        VARCHAR,
    player_id       BIGINT,
    team_id         VARCHAR,
    outcome         VARCHAR,
    kills           INT,
    deaths          INT,
    assists         INT,
    damage_dealt    INT,
    match_duration  INT,
    game_mode       VARCHAR,
    region          VARCHAR,
    completed_at    TIMESTAMPTZ
)
WITH (
    connector     = 'kafka',
    topic         = 'game.match.results',
    properties.bootstrap.server = 'kafka:9092',
    scan.startup.mode = 'latest'
)
FORMAT PLAIN ENCODE JSON;

Also ingest existing player rating data from the operational database via CDC so the streaming layer has historical context:

CREATE SOURCE player_ratings (
    player_id       BIGINT PRIMARY KEY,
    current_mmr     FLOAT,
    games_played    INT,
    region          VARCHAR,
    preferred_mode  VARCHAR,
    last_updated    TIMESTAMPTZ
)
WITH (
    connector     = 'postgres-cdc',
    hostname      = 'postgres.internal',
    port          = '5432',
    username      = 'rwuser',
    password      = '${secret}',
    database.name = 'game_db',
    schema.name   = 'public',
    table.name    = 'player_ratings'
);

Computing Live Performance Metrics

Build a materialized view that tracks rolling performance metrics used by the matchmaking algorithm:

CREATE MATERIALIZED VIEW player_performance_live AS
SELECT
    window_start,
    window_end,
    player_id,
    game_mode,
    region,
    COUNT(*)                                                AS matches_in_window,
    COUNT(*) FILTER (WHERE outcome = 'win')                 AS wins,
    COUNT(*) FILTER (WHERE outcome = 'loss')                AS losses,
    ROUND(
        COUNT(*) FILTER (WHERE outcome = 'win')::DECIMAL /
        NULLIF(COUNT(*), 0) * 100, 2
    )                                                       AS win_rate_pct,
    AVG(kills)                                              AS avg_kills,
    AVG(deaths)                                             AS avg_deaths,
    AVG(damage_dealt)                                       AS avg_damage,
    ROUND(AVG(kills)::DECIMAL / NULLIF(AVG(deaths), 0), 2) AS kd_ratio
FROM HOP(match_results, completed_at, INTERVAL '30 minutes', INTERVAL '7 days')
GROUP BY window_start, window_end, player_id, game_mode, region;

The seven-day HOP window keeps performance metrics fresh by sliding forward every 30 minutes. A player's win rate reflected in this view is based on their last 7 days of activity, not a static all-time average.

Building the Matchmaking Score

Combine the live performance data with the current MMR from the player ratings source using a temporal join:

CREATE MATERIALIZED VIEW matchmaking_profile AS
SELECT
    p.player_id,
    p.current_mmr,
    p.region,
    p.preferred_mode,
    COALESCE(m.win_rate_pct, 50.0)                          AS recent_win_rate,
    COALESCE(m.kd_ratio, 1.0)                               AS recent_kd_ratio,
    COALESCE(m.matches_in_window, 0)                        AS recent_matches,
    COALESCE(m.avg_damage, 0)                               AS recent_avg_damage,
    (
        p.current_mmr * 0.7 +
        COALESCE(m.win_rate_pct, 50.0) * 3 +
        COALESCE(m.kd_ratio, 1.0) * 50
    )                                                       AS composite_score
FROM player_ratings FOR SYSTEM_TIME AS OF NOW() p
LEFT JOIN player_performance_live m
    ON p.player_id = m.player_id
    AND p.region   = m.region;

The composite_score column blends long-term MMR with recent performance signals. Your matchmaking service queries this view to find candidates within a target composite score range, resulting in fairer match composition.

Comparison: Matchmaking Data Pipeline Approaches

ApproachMetric FreshnessScalabilityIntegration EffortMatch Quality
Static MMR (daily recalc)24 hoursHighLowPoor (stale data)
In-memory rating cacheMinutesMediumHigh (cache invalidation)Medium
Stream processing (Flink)SecondsHighVery HighGood
RisingWave streaming SQLSub-secondHighLow (Postgres interface)Excellent

Pushing Updated Profiles to the Matchmaking Service

When a player's matchmaking profile changes significantly, sink the update to Kafka so the matchmaking service can react:

CREATE SINK matchmaking_profiles_sink
FROM matchmaking_profile
WITH (
    connector = 'kafka',
    topic = 'game.matchmaking.profiles',
    properties.bootstrap.server = 'kafka:9092'
)
FORMAT UPSERT ENCODE JSON (
    force_append_only = false
);

The matchmaking service subscribes to this topic and updates its in-memory candidate pool incrementally. Profile changes propagate in under a second.

Monitoring Queue Health

Define a view that helps operations teams monitor matchmaking queue fairness and wait times:

CREATE MATERIALIZED VIEW queue_health AS
SELECT
    window_start,
    window_end,
    region,
    game_mode,
    COUNT(DISTINCT match_id)        AS matches_completed,
    AVG(match_duration)             AS avg_match_duration_secs,
    STDDEV(match_duration)          AS match_duration_stddev,
    AVG(kills + deaths + assists)   AS avg_combat_activity
FROM TUMBLE(match_results, completed_at, INTERVAL '5 minutes')
GROUP BY window_start, window_end, region, game_mode;

High standard deviation in match duration is often a signal that match quality is suffering — one team is dominating quickly. Wire this view to a Grafana dashboard via the Postgres connector for live operations monitoring.

FAQ

Q: How does RisingWave handle regions with different player population densities? A: Partition your matchmaking profile view by region. Low-population regions naturally have fewer candidates, and you can widen the composite score tolerance in your matchmaking query for those regions without changing the underlying pipeline.

Q: Can I run A/B tests on different matchmaking algorithms with this setup? A: Yes. Create multiple composite score materialized views with different weighting formulas. Your matchmaking service reads from whichever view corresponds to the algorithm assigned to the current player.

Q: What if a player has no recent match history? A: The COALESCE fallbacks in the matchmaking profile view assign neutral defaults (50% win rate, 1.0 KD ratio) for new players. These defaults direct them toward average-skill lobbies until enough data accumulates.

Q: How do I prevent MMR manipulation through intentional losses? A: Create a separate materialized view tracking consecutive loss patterns and surface it as a flag in the matchmaking profile. The enforcement team can weight down the MMR of accounts showing manipulation signatures.

Q: Does RisingWave support the throughput needed for a large competitive title? A: Yes. RisingWave has been deployed in production handling millions of events per second. Horizontal scaling adds more compute nodes, and the shared-nothing architecture keeps performance linear as load increases.

Build Fairer Lobbies, Faster

Great matchmaking turns casual players into regulars and competitive players into devotees. With RisingWave, your matchmaking data pipeline stays current with every match completed — so the next lobby is always the fairest one yet.

Start building at https://docs.risingwave.com/get-started and connect with other game engineers at https://risingwave.com/slack.

Best-in-Class Event Streaming
for Agents, Apps, and Analytics
GitHubXLinkedInSlackYouTube
Sign up for our to stay updated.