Guide

Percentage Rollouts

Release a feature to a slice of your users, 1%, 10%, 50%, with no code changes. ShipSilently keeps each user's bucket sticky so refreshes never flip them between variants.

A rollout belongs to a rule, not to a flag. There is no flag-level rollout switch. To ramp a flag to 10% of everyone, add a rule with no conditions and a rollout of 10.

Configure a rollout

  1. Open the flag in the dashboard.
  2. Add a rule (or edit an existing one) and set the value it serves.
  3. Tick gradual on that rule and set the percentage (0–100).
  4. Save. New evaluations honor the rollout within seconds.

How bucketing works

The bucket is derived from the user and the flag together: sha256("{userId}:{flagId}"), whose first 8 hex characters are read as an integer and taken mod 100. A user is in the rollout when their bucket is below the configured percentage.

  • Sticky: same user, same flag, same bucket, every time.
  • Per-flag: the flag's id is in the hash, so a user can be in flag A's 10% and outside flag B's.
  • Monotonic: because membership is bucket < percentage, raising 10 → 20 only ever adds users. Nobody who was in gets dropped.
  • Identical everywhere: the server and every SDK run the same hash, verified by a differential test, so local evaluation agrees with the API exactly.
Bucketing reads userId. If userId is absent it falls back to email, and then to the empty string, which puts every anonymous user in the same bucket. They will all be in or all be out together. Always pass a stable identifier for traffic you intend to split.

Rules and rollouts together

Rules are evaluated in order. For each rule the engine first checks the conditions, then the rollout:

  1. Conditions don't match → skip to the next rule.
  2. Conditions match, no rollout → serve this rule's value (rule_match).
  3. Conditions match, inside the rollout → serve this rule's value (rollout).
  4. Conditions match, outside the rollout → fall through to the next rule.

That last step is the one that surprises people, and it is what makes staged rollouts composable. Missing a rule's percentage does not end evaluation; the user is still eligible for everything below it.

Worked example

Internal staff get the feature outright; everyone else ramps at 10%:

rules (conceptual)
[
  {
    "name": "internal staff",
    "conditions": [{ "attribute": "email", "operator": "ends_with", "value": "@yourcompany.com" }],
    "serveValue": true,
    "rolloutPercentage": null      // everyone who matches, no sampling
  },
  {
    "name": "public ramp",
    "conditions": [],              // matches everyone
    "serveValue": true,
    "rolloutPercentage": 10        // 10% of them get true
  }
]

The other 90% fall past both rules and receive the flag's default value with reason default.

Recommended ramp

  1. 1%: smoke test in production.
  2. 5%: surface common errors.
  3. 25%: load and performance signal.
  4. 100%: full release. Keep the flag for the kill switch.

Because bucketing is monotonic, each step is strictly additive, so a user who has already seen the new behavior never loses it as you ramp up. Ramping down does remove users, so treat that as a rollback rather than a tuning knob, and prefer the kill switch when you need to stop a release immediately.

Verifying a rollout

Check a specific user's outcome before you widen the ramp. The reason tells you which mechanism decided:

terminal
curl -X POST https://api.shipsilently.com/v1/evaluate \
  -H "X-API-Key: $SHIPSILENTLY_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "flagKey": "new-checkout-v2", "context": { "userId": "u_123" } }'

# { "flagKey": "new-checkout-v2", "value": true, "reason": "rollout", "ruleId": "…" }