DEV Community

Cover image for How We Built Sub-0.2ms Player Segmentation That Actually Works in Live Games
AUXTA
AUXTA

Posted on • Originally published at dev.to

How We Built Sub-0.2ms Player Segmentation That Actually Works in Live Games

Most game studios think they have a segmentation problem. They don't — they have a timing problem.
I've seen this pattern over and over: the data is there, the dashboards look good, the "whale / dolphin / minnow" buckets exist. But none of it fires fast enough to actually influence a player's decision during an active session. By the time a batch job reclassifies someone as "at-risk," they've already uninstalled.
This post walks through the architectural approach behind Auxta DB — a real-time behavioral classification engine we built specifically to fix this — and shows what the actual integration looks like in TypeScript.

Auxta player segmentation gaming

The Core Problem: Batch Pipelines vs. Session Windows

Here's the brutal truth about rule-based segmentation in live games:

Daily batch job fires at 2am
  → Player classified as "at-risk" at 2:03am
  → Push notification queued
  → Player opens app at 9am, already disengaged
  → Notification fires into the past
Enter fullscreen mode Exit fullscreen mode

The monetization moment and the churn signal are session-level events. Classification that doesn't operate within the session is just reporting dressed up as decisioning.
The three failure modes we see consistently:

  • Coarseness — "whale" describes two completely different players who need opposite treatment
  • Latency — batch pipelines run on a different timescale than player decisions
  • Fragmentation — behavioral data lives in 3–4 separate systems, never assembled in real time

The Architecture: Players as Behavioral Vectors

Auxta represents every player as a multi-dimensional vector — a live, structured snapshot of every behavioral parameter relevant to your game. Not a fixed schema. Not a row in a table. A vector that updates in real time as gameplay events occur.
Here's what a player vector definition looks like:

// PlayerVector.ts — defines the full behavioral profile for a player.
// Static fields (country, acquisitionSource) are set on registration.
// Computed fields (winRateLast10, consecutiveLosses, etc.)
// are updated by the game server on every session event — no batch job required.

class PlayerVector extends AuxtaVector {
    country: string;
    region: string;
    acquisitionSource: string;       // 'organic' | 'paid_ua' | 'influencer' | ...

    totalSpend: number;              // lifetime, updated on every purchase event
    totalRounds: number;             // lifetime, incremented per round completion
    lastLoginTimestamp: number;      // unix ms, updated on every session open
    sessionFrequency7d: number;      // computed: sessions in rolling 7-day window
    averageSessionLength: number;    // computed: minutes, rolling 30-day average

    winRateLast10: number;           // computed: 0.0–1.0, last 10 sessions
    consecutiveLosses: number;       // live: resets on win, increments on loss
    currentLevel: number;            // live: updated on level completion event
    lastInGameAction: string;        // live: 'shop_open' | 'level_fail' | 'ad_watch' | ...

    preferredGameMode: string[];     // computed: top modes by session share last 14d
    lastPurchasedItemCategory: string;
    daysSinceLastPurchase: number;

    churnRiskScore: number;          // 0.0–1.0, recomputed on each session close
    predictedLTV: number;
}
Enter fullscreen mode Exit fullscreen mode

Upserting a player profile on session close is a single typed operation:

const playerVector = new PlayerVector({
    country: 'DE',
    totalSpend: 142.50,
    totalRounds: 387,
    lastLoginTimestamp: Date.now(),
    sessionFrequency7d: 6,
    winRateLast10: 0.4,
    consecutiveLosses: 3,
    lastInGameAction: 'level_fail',
    preferredGameMode: ['pvp', 'ranked'],
    churnRiskScore: 0.61,
    predictedLTV: 280,
    // ... other dimensions
});

const updateCommand = new AuxtaCommand().add(playerVector);
await auxta.query(updateCommand);
// Player is now queryable across all 100+ dimensions with zero propagation delay
Enter fullscreen mode Exit fullscreen mode

The Query That Makes It Real

Here's where it gets interesting. Instead of waiting for a batch job, the game server issues a typed search query during the active session — resolving in under 0.2ms.
This fires in real time when the server records consecutiveLosses === 3:

// Fired in real time when the game server records consecutiveLosses === 3.
// Resolves in under 0.2ms. Result drives immediate offer injection
// into the post-round screen — no async pipeline, no delayed push.

const recoveryOfferSegment = new AuxtaCommand()
    .search(PlayerVector)
    .where('consecutiveLosses', losses => losses.gte(3))
    .where('daysSinceLastPurchase', days => days.gte(3).and().lte(14))
    .where('lastInGameAction', action => action.match('level_fail'))
    .where('churnRiskScore', risk => risk.gte(0.5).and().lte(0.8))
    .where('preferredGameMode', mode => mode.in(['pvp', 'ranked']));

const eligiblePlayers = await auxta.query<PlayerVector>(recoveryOfferSegment);
Enter fullscreen mode Exit fullscreen mode

Notice what this query combines in a single operation:

  • A stable dimension (preferredGameMode — computed preference over 14 days)
  • A slowly-changing dimension (daysSinceLastPurchase — monetization recency)
  • Two live computed dimensions (consecutiveLosses and lastInGameAction — present-tense gameplay state)

Same approach for premium offer targeting:

const premiumOfferSegment = new AuxtaCommand()
    .search(PlayerVector)
    .where('totalSpend', spend => spend.gte(50))
    .where('sessionFrequency7d', freq => freq.gte(5))
    .where('winRateLast10', rate => rate.gte(0.6))
    .where('daysSinceLastPurchase', days => days.gte(5))
    .where('acquisitionSource', src => src.in(['organic', 'influencer']))
    .where('lastPurchasedItemCategory', cat =>
        cat.match('booster').or().match('currency_pack')
    );
Enter fullscreen mode Exit fullscreen mode

This surfaces players who are: established spenders, highly engaged this week, on a winning streak, purchase-lapsed enough to be receptive, from high-quality acquisition sources, and with a purchase history in high-margin categories — all in one sub-millisecond query.


The Reindexing Problem Nobody Talks About

Here's a technical constraint that kills most traditional approaches: reindexing.
When a new game mode launches, a new economy mechanic ships, or a new event type becomes relevant — any column-store or document database with a fixed index structure needs a reindex operation. In a live game with millions of player records, that reindex takes hours.
For a seasonal event launch where the new mechanic is the entire basis of the LiveOps campaign, that's not a minor inconvenience — it forces teams to either plan campaigns around database maintenance windows or accept degraded targeting at launch.
Auxta's vector model eliminates this entirely. Because player data is stored as dimensional vectors rather than rows with fixed column indexes, adding a new parameter requires no reindex operation. The new dimension is immediately available for queries after the first player vector is written with it populated. Existing players without the new parameter simply go unmatched on that dimension — no errors, no migration job.

Integration Model

Auxta slots into an existing gaming stack without a data platform overhaul:

  • Game server → Auxta: Write vector updates on session events (session open, round complete, purchase, session close)
  • Offer engine / LiveOps platform → Auxta: Query for segment membership at decisioning moments (shop open, post-round screen, push trigger, event eligibility)

The Auxta UI lets LiveOps managers modify segment definitions directly — dimensional thresholds, combination operators, inclusion/exclusion conditions — without engineering involvement. Changes take effect on the next query. No deployment cycle. No pipeline reconfiguration.


What This Fixes (With Numbers)

The revenue inefficiency from coarse, batch-based segmentation typically sits between 10–30% of addressable revenue across a live game — not theoretical, but as a measured gap between what current segmentation-driven decisions produce vs. what the same player base generates under correctly timed classification.
Closing the three gaps (coarseness, latency, fragmentation) produces:

  • ARPU improvement — offer type, value, and timing match the player's current behavioral state, not a stale archetype
  • LTV improvement — retention triggers fire on behavioral signal, not schedule
  • CAC payback shortens — newly acquired players are classified from their first session, not held in "new user" limbo for 30 days
  • Bonus efficiency improves — precise micro-segmentation excludes players whose behavioral profile makes them unlikely to convert regardless of incentive

The bottom line: weak player segmentation isn't a data problem. It's a decision-timing problem caused by the absence of real-time behavioral classification. That's exactly what Auxta DB was built for.
If you're running a live game at scale and want to evaluate the integration, the ADAAS team runs scoped technical assessments — typically two weeks from kickoff to live classification in production.

Top comments (0)