Skip to content

Artificial intelligence · 3 July 2026

Training a Model for Every Customer: Per-Tenant ML at Scale

Most AI-for-gaming products score everyone through one pooled model. We argue for the opposite: every customer gets its own models, trained on its own data, scored in its own isolated world.

Written by

Pancham Narang

6 min read

Most AI-for-gaming products ship one model trained on a pooled dataset and score everyone through it. That is convenient for the vendor and wrong for the customer, because an industry average is a model of no one: confidently mediocre for every game and badly wrong for the ones least like the average. The alternative is per-tenant, multi-tenant machine learning in which every customer gets its own models, trained on its own data, scored in its own isolated world. This note is about how to make that safe, how to keep it fast, and how to handle the customer who does not yet have enough data without lying about it.

Isolation you can prove, not just promise

The frightening failure mode of multi-tenancy is a query that forgets to scope itself to the right customer and quietly returns another customer's players. Application-code scoping catches most of that, but most is not a word you want anywhere near a privacy guarantee. So on the production datastore we push isolation down into the database itself, using row-level security so that a row is visible only when it belongs to the connection's current tenant.

Three details make this real rather than decorative. First, the policy is forced to bind even the table owner, because an unforced policy is silently bypassed by the owner account the application often connects as. Second, the same rule governs writes as well as reads, so a scoped connection can neither read nor write another tenant's rows even when the application logic above it is buggy. Third, the tenant identity is set per connection and reset when that connection returns to the pool, so a pooled connection can never carry one customer's context into another customer's request.

The trusted backend still needs legitimate cross-tenant access for genuine platform work: training a shared model, provisioning a new customer, running fleet-wide health checks. Rather than connecting as a superuser everywhere, those specific operations run under a recognised platform context that the policies treat as authorised. That set of cross-tenant methods is small, named and auditable, not the default path. It is defence in depth: the application scopes every query, and the database refuses anything the application got wrong.

Scoring on the customer's own history, kept fast

With isolation handled, the per-tenant pipeline is simple in shape. For a given customer, pull its curated activity, train its churn, value and recommendation models, and cache the trained bundle. The interesting part is the cache. Training a full model suite is not something you want to do on a console page-load, yet you also cannot serve stale scores after new data lands. So the cache key is tied to the customer's own data, in practice to its row count and latest ingest marker, which gives exactly the right invalidation behaviour: a new batch of data changes the key and triggers a re-score, while a hundred console page-views in a row all reuse one trained bundle.

Concurrency is the other half. If a serving request and a background refresh both miss the cache for the same customer at the same instant, you do not want them both training the same models and racing to write the result. A per-customer single-flight lock means the first caller trains and every other caller for that customer waits for the in-flight result instead of starting a duplicate. Crucially the lock is per customer, so a large customer's slow retrain never blocks a small customer's fast one; they live in separate lanes.

We also schedule re-scoring proactively and pre-warm the bundle after an ingest, so that by the time a product manager opens the console the current scores are usually already sitting in cache. Fast and fresh are not in tension once the cache is keyed to the data rather than to the clock.

The honest part: cold-start

Here is where a lot of platforms quietly cheat. A brand-new customer with two weeks of data cannot train a reliable churn model, because there are simply not enough resolved outcomes yet to learn from. The tempting thing is to score them anyway and hope no one checks the calibration. We refuse to.

A small customer is handled in one of two honest ways. If a validated cross-tenant model exists, trained on the pooled data of larger, consenting customers or seeded from a strong, well-founded prior, and shown to actually validate on a held-out slice, then the small customer's players are scored through a blended approach, and the scores ship with calibrated confidence bands so the product team can see how much to trust each one. If no such validated model exists, the platform returns an explicit insufficient-data state and says so plainly in the console. It does not manufacture a number to fill the cell.

That single choice, to say "I do not know" rather than guess, has real architectural consequences. The cross-tenant model is only allowed to fire when it genuinely validates on the target; a weak prior that fails its own holdout is not permitted to masquerade as signal. It also surfaces a strategic question we name for the customer rather than hide: is this a multi-tenant world where sibling customers can lift each other's cold-start, or a single-tenant deployment that needs a strong prior shipped alongside it? Either is a fine answer. Pretending a lone small dataset yields production-grade scores is not.