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 only on its device_id (which you control) and the deployment's seed (which is auto-generated).

The function

def cohort_bucket(seed: str, device_id: str) -> int:
    """Return an integer in [0, 100)."""
    blob = f"{seed}:{device_id}".encode()
    digest = hashlib.sha256(blob).digest()
    # Take the first 8 bytes as a big-endian unsigned int, mod 100.
    return int.from_bytes(digest[:8], "big") % 100


def is_in_rollout(seed: str, device_id: str, percentage: int) -> bool:
    return cohort_bucket(seed, device_id) < percentage

A device is "in the cohort" if cohort_bucket(...) < percentage.

Properties

  • Uniform: over a large fleet, cohort_bucket distributes evenly across [0, 100).
  • 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   → ~5% of fleet exposed (buckets 0..4)
t=1h  seed=S, percentage=25  → adds buckets 5..24 (cumulative 25%)
t=4h  seed=S, percentage=100 → all buckets included

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

Worked example

>>> cohort_bucket("seed-abc", "esp32-0001")
17
>>> is_in_rollout("seed-abc", "esp32-0001", 5)
False
>>> is_in_rollout("seed-abc", "esp32-0001", 25)
True

Device esp32-0001 is in bucket 17. It's not in a 5% canary, is in a 25% rollout, and stays exposed forever after that point (as long as the seed doesn't change).