Feature Gates
A feature gate sits in front of an experiment and decides who sees the bandit and what they see when they don't. Gates give you the safety rails you'd expect from a production rollout system — percentage rollouts, targeting rules, schedules, and a default arm for everyone outside the experiment.
Every experiment can have at most one gate. Without a gate, every selection request goes straight to the bandit policy.
What a Gate Controls
| Control | Description |
|---|---|
| Rollout % | Fraction of traffic routed to the bandit. The remainder gets the default arm. Hashed by request ID for sticky assignment. |
| Default arm | The arm served when a request is excluded by the gate (rollout, schedule, or unmatched rules). |
| Targeting rules | Match on context metadata fields (country, device, plan_tier, etc.) and route matching traffic to a specific arm or to the bandit. |
| Schedule | Start/end dates and active hours of day. Outside the window, the gate serves the default arm. |
| Enabled flag | Master switch — flipping it off bypasses the bandit entirely. |
Create an Experiment with a Gate
from qbrix import Qbrix
client = Qbrix()
experiment = client.experiment.create(
name="checkout-banner",
pool_id=pool.id,
policy="auto",
policy_params={"reward_type": "binary"},
feature_gate={
"enabled": True,
"rollout_percentage": 25.0,
"default_arm_id": pool.arms[0].id,
"rules": [
{
"key": "country",
"operator": "in",
"value": ["US", "GB", "DE"],
"arm_id": pool.arms[1].id,
},
],
},
)curl -X POST $QBRIX_URL/api/v1/experiments \
-H "X-API-Key: $QBRIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "checkout-banner",
"pool_id": "<pool-id>",
"policy": "auto",
"policy_params": {"reward_type": "binary"},
"feature_gate": {
"enabled": true,
"rollout_percentage": 25.0,
"default_arm_id": "<default-arm-id>",
"rules": [
{
"key": "country",
"operator": "in",
"value": ["US", "GB", "DE"],
"arm_id": "<variant-a-arm-id>"
}
]
}
}'How Selection Resolves
When a select request hits a gated experiment, qbrix walks the gate in this order:
- Enabled? If the gate is off, return the default arm.
- Schedule check. If outside the configured window, return the default arm.
- Rule match. Iterate rules in order. The first matching rule with an
arm_idwins (returns that arm directly). A rule withoutarm_idopts the request into the bandit. - Rollout hash. Hash the request ID and bucket into
[0, rollout_percentage). In-bucket requests get the bandit's selection; out-of-bucket get the default arm.
The response includes an is_default: true flag so your client can distinguish bandit picks from gate fall-throughs.
Targeting Operators
| Operator | Example |
|---|---|
eq / neq | {"key": "plan", "operator": "eq", "value": "pro"} |
in / not_in | {"key": "country", "operator": "in", "value": ["US", "CA"]} |
contains / not_contains | {"key": "user_agent", "operator": "contains", "value": "Mobile"} |
gt / gte / lt / lte | {"key": "tenure_days", "operator": "gte", "value": 30} |
Rules read context fields from Context.metadata, so make sure your select() calls include the keys your rules reference.
Common Patterns
Canary launch. Start at rollout_percentage=5, watch insights, ramp to 25 → 50 → 100.
Holdout group. Set the default arm to control and rollout to 90% — 10% of traffic always sees the control regardless of what the bandit learns.
Geography-scoped experiments. Use a single country in [...] rule with arm_id unset, so only matching traffic enters the bandit; everyone else gets the default arm.
Scheduled flash test. Set start_date / end_date and active_hours_start / active_hours_end to constrain the experiment to a marketing window.
Gates can be created and updated independently of the experiment via /api/v1/gates. See the API Reference for the full schema.
Next Steps
- Auto Policy — let qbrix pick the algorithm
- Pools & Experiments — the data model behind every gate
- Console Experiments — manage gates from the UI