How Bucketing Works
Bucketing is the mechanism behind percentage rollouts.
When a flag has a percentage rollout, Feature Flags needs a way to decide whether a visitor qualifies to see the feature. It does this by creating a “bucket” number for every visitor and checking whether than number falls within the rollout range.
This page explains how bucketing works, what controls the bucket input, and how to use bucket keys to change the unit of a rollout.
Creating a Bucket #
The plugin hashes a bucket key together with the flag handle to produce a number from 0 to 99:
return abs(crc32($bucketKey . ':' . $handle)) % 100;
A flag with rollout percentage of 30 enables for any visitor whose bucket is 0 – 29. This means:
- The same input always produces the same bucket for the same flag. A visitor who qualifies at 30% will still qualify when you increase to 50%.
- Raising the percentage from 25% to 50% adds buckets 25 – 49. Nobody in 0 – 24 loses access. Lowering the percentage does remove visitors from the high end.
- The flag handle is part of the hash, so a visitor at bucket 12 for
flag-awon’t necessarily be at bucket 12 forflag-b. Different flags get independent distributions. - There’s no database table tracking cohorts. The bucket is computed on the fly from the input string.
Bucket keys #
The bucket key parameter lets you change what the rollout is “per” (or the unit of the percentage).
The bucket key is hashed with the flag handle, and the plugin resolves it in this order:
- Explicit bucket key: if you pass one to
isEnabled(), it’s used directly - User ID: if the visitor is logged in, their Craft user ID (as a string)
- Anonymous visitor cookie: a UUID stored in the
_ff_vidcookie - Nothing available: the rollout check is skipped and the flag returns
false
This resolution happens automatically. If you don’t pass a bucket key and the visitor is logged in, their user ID is the bucket input. If they’re logged out and have the visitor cookie, that UUID is the input.
Default: per-user
Without a explicit bucket key passed in to isEnabled(), each visitor gets their own bucket. Logged-in users are bucketed by their user ID, and anonymous visitors by their cookie UUID. These are independent pools: a 30% rollout means roughly 30% of logged-in users and roughly 30% of anonymous visitors, computed separately. Two visitors on the same page may see different results.
{# 30% of users see the new checkout #}
{% if craft.featureFlags.isEnabled('new-checkout') %}
Per-entry
Pass entry.id as the bucket key, and the rollout applies across entries instead of users. 30% of your entries will have the flag enabled, and every visitor to a given entry sees the same result.
{# 30% of entries show the experimental hero layout #}
{% if craft.featureFlags.isEnabled('hero-experiment', entry.id) %}
This is useful for content-level experiments, like testing a new layout on a subset of blog posts or product pages.
Any string
The bucket key can be any string. The plugin doesn’t interpret it, it just hashes it. Use whatever dimension makes sense for your rollout.
Bucket keys and targeting rules #
Bucket keys only affect the percentage rollout step of evaluation. They have no effect on:
- Whether the flag is enabled or disabled
- Whether a targeting rule matches
- Whether the flag has expired
If a flag has no rolloutPercentage, the bucket key is ignored entirely. If a targeting rule matches and the flag uses the default all strategy, the bucket key is also irrelevant. The rule match short-circuits before the rollout check.
With the rule strategy, the bucket key matters even when a rule matches, because the rollout percentage filters within the matched group. See Percentage Rollouts for details on how the two strategies interact.
The bucket key is a replacement, not a filter #
This is an important distinction: when you pass a bucket key, it replaces the user’s identity in the hash entirely. It doesn’t combine with it. Whatever string you pass becomes the only thing that determines the outcome.
This is intuitive when the bucket key is entry.id because you want every visitor to the same entry to see the same result. The entry ID replaces the user ID, and the rollout splits across entries instead of users.
But it’s surprising when the bucket key has few possible values. Say you pass a user group ID as the bucket key, expecting “20% of each group.” That’s not what happens. Every user in the same group passes in the same key (the user group id), so they all compute the same bucket number, so they all get the same result. The plugin can’t give 20% of a group the feature because every user in that group is identical from the bucket’s perspective.
For example, if your site has 4 user groups and you use the group ID as the bucket key with a 20% rollout:
| Group ID | Bucket | Included? |
|---|---|---|
| 1 | 90 | No |
| 2 | 66 | No |
| 3 | 77 | No |
| 4 | 46 | No |
None of the 4 bucket numbers fall below 20, so 0% of users see the feature, not 20%. At 50%, only group 4 (bucket 46) qualifies, meaning 100% of that group sees it and 0% of the other three. It’s all-or-nothing per group because there are only 4 possible outcomes.
Choosing good bucket keys #
Bucket keys work best with many distinct values. User IDs, entry IDs, UUIDs, and session IDs all produce hundreds or thousands of distinct bucket numbers, giving a smooth approximation of the target percentage.
Low-cardinality inputs, like group IDs, plan tiers, locale codes, boolean fields, produce only a handful of bucket numbers. The percentage can’t distribute smoothly across a small number of values. A 50% rollout across 4 values gives you all-or-nothing per value, not 50% of each.
Good bucket keys
| Bucket key | Unit of rollout | Why it works |
|---|---|---|
| User ID (default) | Per-user | Potentially thousands of distinct values |
| Entry ID | Per-entry | Each piece of content gets its own bucket |
| Session ID | Per-session | Consistent within a visit, re-rolls next time |
| Account/organization ID | Per-company | All users in the same org see the same result |
| Order/cart ID | Per-order | Consistent experience throughout a transaction |
| Anonymous visitor cookie | Per-visitor | Built-in default for logged-out traffic |
Suboptimal bucket keys
| Bucket key | Problem |
|---|---|
| User group ID | Most sites have 3 – 10 groups. All-or-nothing per group. |
| Subscription plan handle | A handful of tiers. Same issue. |
| Locale / language code | Typically 2 – 10 values. Entire locales get included or excluded. |
Boolean field ("yes" / "no") | Two values, two buckets. No percentage control at all. |
| Entry type handle | A few distinct values. Rollout can’t distribute smoothly. |
| Environment name | 2 – 4 values. Use an environment targeting rule instead. |
For low-cardinality dimensions, use targeting rules instead of bucket keys. Rules let you select a specific group, plan, or environment directly. If you need a percentage within that group, combine the rule with a percentage rollout. The rollout will bucket by individual user ID (the default), giving a smooth distribution.
Anonymous visitors #
For logged-out visitors with no explicit bucket key, the plugin generates a UUID and stores it in a first-party cookie so the same visitor gets the same bucket across page views and return visits.
Set anonymousCookieTtl to 0 to disable anonymous visitor tracking. Percentage rollouts will then only work for logged-in users or when an explicit bucket key is provided.