Skip to content

Cohorts and rollout math

Rollouts in SimpleOTA are deterministic. Same (seed, device_id) always produces the same cohort bucket. This means:

  • Ramping percentage from 525 only adds devices to the exposed set; it never re-rolls and demotes anyone.
  • Re-creating a deployment with the same seed reproduces the same decisions, useful for surgical retries.
  • A device's exposure depends on its database UUID (device.id, assigned at registration) and the deployment's seed (auto-generated). Note this is not the device_id string your firmware reports, so you cannot steer cohort assignment by choosing device names.

The function

The implementation lives in deployments/rollout.py:

BUCKET_RESOLUTION = 10_000  # 0.01% granularity


def stable_bucket(rollout_seed, device_id) -> int:
    """Return a stable integer in [0, BUCKET_RESOLUTION)."""
    payload = f"{rollout_seed}:{device_id}".encode("utf-8")
    digest = hashlib.sha256(payload).digest()
    n = int.from_bytes(digest[:8], byteorder="big")
    return n % BUCKET_RESOLUTION


def is_in_rollout(rollout_seed, device_id, rollout_percentage) -> bool:
    if rollout_percentage <= 0:
        return False
    if rollout_percentage >= 100:
        return True
    threshold = rollout_percentage * (BUCKET_RESOLUTION // 100)
    return stable_bucket(rollout_seed, device_id) < threshold

The 10,000 buckets give rollouts 0.01% granularity. A device is in the cohort when its bucket is below percentage * 100. SHA-256 is used rather than Python's hash() so the result is reproducible across processes and versions.

Properties

  • Uniform: over a large fleet, stable_bucket distributes evenly across [0, 10000). Sampled over 20,000 devices the mean bucket is ~4,991 against an ideal of 5,000, and a 5% rollout catches ~5.3%.
  • Stable: fixing the seed, the same device always lands in the same bucket forever.
  • Independent across deployments: a different seed re-rolls the bucket, so a device's exposure to deployment A says nothing about its exposure to deployment B.

Ramping safely

t=0   seed=S, percentage=5   → buckets 0..499     (~5% of fleet)
t=1h  seed=S, percentage=25  → adds buckets 500..2499  (cumulative 25%)
t=4h  seed=S, percentage=100 → every device, short-circuited to True

Anyone exposed at t=0 is still exposed at t=1h; buckets 0..499 are strictly inside buckets 0..2499. Never change the seed mid-rollout or this property breaks.

Worked example

>>> seed = "3f2a9c10-5b7e-4a12-9f3d-2b8c1e4d7a55"   # deployment.rollout_seed
>>> dev  = "a1b2c3d4-0000-4000-8000-000000000001"   # device.id
>>> stable_bucket(seed, dev)
5370
>>> is_in_rollout(seed, dev, 5)      # threshold 500
False
>>> is_in_rollout(seed, dev, 25)     # threshold 2500
False
>>> is_in_rollout(seed, dev, 100)
True

This device sits in bucket 5,370, a little past halfway. It stays out of the rollout until the percentage passes 53.7%, and once it is in, it stays in as long as the seed does not change.