Contents
- What does Laravel Pennant actually do?
- Why does gradual rollout reduce deployment risk?
- How do percentage-based rollouts work in Pennant?
- What can feature flags do beyond shipping new features?
- How does Pennant store and manage flag state?
- How do you retire a flag cleanly?
- When should you add flags to an existing project?
Key Takeaways
- Laravel Pennant is the framework's first-party feature flag package — built in since Laravel 10.9, no third-party service required
- Feature flags let you decouple deployment from release: ship the code when it's ready, expose the feature to users when you're confident
- Percentage-based rollouts with
Lottery::odds()give each user a consistent experience — they won't flip between old and new UI on repeat visits- Pennant stores flag results per-user in a
featuresdatabase table; flags can also be scoped to teams, accounts, or any Eloquent model- Retiring a flag is as important as creating one —
php artisan pennant:purgeclears stored values for flags you're removing from the codebase
Feature flags feel like something only big tech companies need. Deploy when ready, ship to everyone, move on. That works until it doesn't. One bad release to your entire user base is all it takes to reconsider the approach.
Laravel Pennant is the framework's first-party answer to feature flags and A/B testing. It has been in Laravel since version 10.9, and it is simpler to set up and use than most teams expect.
What does Laravel Pennant actually do?
Pennant gives you a clean way to define flag logic, store each user's resolved value, and check the flag anywhere in your application. A feature flag is a conditional: show this new thing to some users, not others. Pennant handles the storage, consistency, and API — you write the business rule.
The basic setup looks like this:
Feature::define('new-checkout-flow', function (User $user) {
return $user->created_at->isAfter(now()->subDays(30));
});
Then in your controller or Blade view:
if (Feature::active('new-checkout-flow')) {
return view('checkout.new');
}
return view('checkout.legacy');
New users get the new flow; everyone else keeps the old one. No custom middleware. No rolling your own session logic. The first time Pennant evaluates a flag for a user, it stores the result. That user always gets the same experience on subsequent visits — consistency that matters when you are testing a meaningful change.
Why does gradual rollout reduce deployment risk?
Every deployment is a bet. You are betting that the change works the way you think it does for every user, on every device, in every edge case. Feature flags let you make smaller, verifiable bets.
Roll the new checkout to 10% of users. Watch your error rates, conversion numbers, and support tickets. If nothing catches fire in 48 hours, bump it to 50%, then 100%. If something does go wrong, you flip the flag off — no rollback, no hotfix deploy, no 2 AM phone call.
This is especially valuable when you are changing something tied to revenue. A new pricing page, a new onboarding flow, a new payment form — these are exactly the changes you do not want to discover are broken after 100% of users have already seen them. The blast radius of a bad release shrinks in direct proportion to the percentage of users you expose it to first.
How do percentage-based rollouts work in Pennant?
Pennant supports lottery-style rollouts out of the box. The modulo approach gives a deterministic, consistent result based on user ID:
Feature::define('redesigned-dashboard', function (User $user) {
return $user->id % 10 === 0; // ~10% of users
});
For a true random distribution, use Pennant's built-in Lottery helper:
use Laravel\Pennant\Feature;
use Illuminate\Support\Lottery;
Feature::define('redesigned-dashboard', Lottery::odds(1, 10));
The Lottery::odds(1, 10) call resolves once per user and stores the result. A user assigned to the test group on their first visit will stay in that group on every subsequent visit. This is the property that makes A/B testing meaningful — without consistent assignment, you are measuring a noisy mix rather than a clean experiment.
What can feature flags do beyond shipping new features?
Teams most often reach for feature flags when shipping something new. But the pattern is equally useful in three other contexts.
Kill switches for third-party integrations. Wrap any integration that has an external dependency — a payment provider, a document generation service, an email API — in a flag. If the vendor has an outage, flip the flag and fall back gracefully without a deployment. The kill switch pattern is a natural companion to gradual rollouts.
Beta programs and early access. Activate a feature only for users who opted into beta access, or for users with a specific email domain for internal testing. Feature::for($user) scopes a check to any model, not just the authenticated user. That makes team-level or account-level flags straightforward:
// Activate only for internal team members
Feature::define('admin-debug-panel', function (User $user) {
return str_ends_with($user->email, '@pixelworx.io');
});
Account-level feature gating. SaaS applications often need to gate features by plan or account type rather than individual user. Pennant handles this cleanly because scope is flexible — pass a team, organisation, or subscription model rather than a user.
How does Pennant store and manage flag state?
By default, Pennant stores flag results in a features database table. Run php artisan pennant:install and the migration is created automatically. Each row contains a feature name, the morphable scope (the user or model), and the resolved boolean or value.
You can also use the array driver for in-memory storage that resets per request — useful in tests or in cases where you do not want persistence. For teams already using a dedicated feature flag service like LaunchDarkly, Pennant supports custom drivers so you can pull flag state from an external source while keeping the same API in your application code.
Checking flags in Blade templates follows the same pattern as controllers:
@feature('new-checkout-flow')
<x-checkout.new />
@else
<x-checkout.legacy />
@endfeature
How do you retire a flag cleanly?
The best part of Pennant is that it encourages cleanup. A flag that runs forever becomes technical debt — a conditional branch nobody remembers the original reason for, sitting in every critical code path indefinitely.
Once a flag is at 100% and stable, the retirement process is two steps: remove the feature definition and the old code path from the codebase, then purge the stored values from the database:
php artisan pennant:purge redesigned-dashboard
This deletes every row in the features table for that flag. The codebase stays clean, and the database stays lean. Building the retirement step into your flag workflow from the start prevents the accumulation of zombie conditionals that make codebases hard to reason about over time.
When should you add flags to an existing project?
The right time to add a Pennant flag to a feature is when you are building it, not when something has already gone wrong in production. Retrofitting flags into a broken system during an incident is the worst possible time — you are already under pressure, and the wrapping logic may itself introduce new paths that need testing.
For any feature that touches a critical user flow — checkout, onboarding, authentication, billing — the overhead of defining a flag at build time is small relative to the control it gives you at release time. Combine Pennant with Laravel Horizon for visibility into queue jobs triggered by feature-gated paths, and with your deployment pipeline to ensure flags are defined before any code using them reaches production.
If you are building something where you want careful rollout control from the start, get in touch. Wiring this in early is far less effort than adding it after the fact.