Laravel's Lottery Class: The Hidden Gem You're Not Using
Coffee Chat - Episode 5

Picture this: It's 3 AM. Your phone buzzes. Then buzzes again. And again. Your error tracker has gone absolutely mental because your production app decided to report every. single. lazy loading violation. All 47,000 of them. In one hour.
Sound familiar?

Don't worry. Laravel has a hidden gem that most developers walk past every single day. It's been sitting there since version 9.40, quietly waiting to save you from notification hell.
Meet the Lottery class.
Wait, Laravel Has a Lottery?
Yes! And no, Taylor Otwell didn't add gambling to the framework (though that would make stand-ups more interesting).
The Lottery class lives in Illuminate\Support\Lottery and does exactly what it sounds like. It lets you run code based on probability. Think of it as a bouncer for your callbacks. Sometimes they get in. Sometimes they don't.
Here's the simplest example:
use Illuminate\Support\Lottery;
$result = Lottery::odds(1, 5)->choose();
// Returns true 20% of the time, false 80% of the time
The odds() method takes two arguments: winning chances and total chances. So odds(1, 5) means 1 in 5 chance, or 20% probability. Simple maths, powerful results.
But here's where it gets spicy. You can attach callbacks:
Lottery::odds(1, 10)
->winner(fn () => logger('π You won!'))
->loser(fn () => logger('π’ Maybe next time'))
->choose();
The winner() callback fires when luck is on your side. The loser() callback runs when it isn't. Finally, choose() rolls the dice and returns whatever your callback returns.
The Real Problem This Solves
Let's get real for a second. Why would you ever want code to run... sometimes?
Here's a scenario every Laravel developer has faced:
You're a responsible developer. You've enabled Model::preventLazyLoading() in production because you read that article about N+1 queries being evil. Good job! But now your Sentry dashboard looks like this:

Your app handles 500,000 requests daily. Each request triggers 3 lazy loading violations on average. That's 1.5 million error reports. Per day.
Your Sentry bill? Through the roof. Your inbox? Destroyed. Your will to live? Questionable.
But here's the thing. You don't NEED 1.5 million reports to know you have a problem. You just need... some of them. Enough to know the issue exists and where it lives.
Enter the Lottery.
Real World Use Cases (The Fun Part)
Let me show you some scenarios where the Lottery class absolutely shines.
1. The "Please Stop Emailing Me" Slow Query Logger
Your database has some... let's call them "personality traits." Sometimes queries take longer than they should. You want to know about it, but not EVERY time.
use Carbon\CarbonInterval;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Lottery;
// In your AppServiceProvider boot() method
DB::whenQueryingForLongerThan(
CarbonInterval::seconds(2),
Lottery::odds(1, 100)->winner(
fn () => report('π Query exceeded 2 seconds. Time for a coffee break!')
)
);
Now only 1% of slow queries get reported. You still catch the problem. Your phone stays quiet. Everyone's happy.
Notice something cool here? We're passing the Lottery instance directly to whenQueryingForLongerThan(). No need to wrap it in a closure or call choose(). That's because...
2. The Magic of __invoke()

Here's where my mind was blown when I first discovered this.
The Lottery class implements __invoke(), which means a Lottery instance IS a callable. You can pass it anywhere that accepts a callback.
From the Laravel documentation:
Since the lottery class is callable, we may pass an instance of the class into any method that accepts callables.
This is HUGE. Laravel has tons of methods that accept callables. And the Lottery class slides right in.
3. The "My Error Tracker Bill Is How Much?!" Solution
Let's say you're importing products from a sketchy CSV file your client sent you. (We've all been there.) Some rows will fail. Many rows will fail. You don't need to report ALL of them.
$products = collect($csvRows);
$failedCount = 0;
$products->each(function (array $row) use (&$failedCount) {
try {
Product::create($row);
} catch (ValidationException $e) {
// Report only 1% of failures to Sentry
Lottery::odds(1, 100)
->winner(fn () => report("Import failed: {$e->getMessage()}"))
->choose();
$failedCount++;
}
});
logger("Import complete. {$failedCount} rows failed.");
Your CSV has 50,000 bad rows? Instead of 50,000 Sentry events (and a very angry finance team), you get around 500. Still enough to debug the patterns, not enough to bankrupt you.
4. The "Let's A/B Test Without Actually Setting Up A/B Testing" Hack
Need to show a new feature to a percentage of users but don't have time to set up a proper feature flag system? I've got you.
public function index()
{
return Lottery::odds(10, 100)
->winner(fn () => view('dashboard.shiny-new-version'))
->loser(fn () => view('dashboard.boring-old-version'))
->choose();
}
10% of users see the new dashboard. 90% see the old one. Is this a proper A/B test with analytics? No. Will it work for a quick experiment before your sprint demo? Absolutely.
5. The "Our Analytics Pipeline Is Crying" Data Sampling
Your data science team wants user behavior analytics. Processing all 2 million users would take 3 days. They need results by tomorrow.
User::chunk(1000, function ($users) {
foreach ($users as $user) {
// Process roughly 5% of users
Lottery::odds(5, 100)
->winner(fn () => AnalyzeUserBehavior::dispatch($user))
->choose();
}
});
Instead of 2 million jobs, you queue around 100,000. Still statistically significant. Finishes overnight. Data scientists buy you coffee.
6. The "Pizza Party Decider" (Yes, Really)
Okay, this one's just for fun. But imagine this in your Slack bot:
$shouldOrderPizza = Lottery::odds(1, 20)
->winner(fn () => "π PIZZA FRIDAY IS TODAY! Order now!")
->loser(fn () => "No pizza today. Back to work.")
->choose();
$slack->send('#general', $shouldOrderPizza);
5% chance of pizza every Friday. The suspense keeps morale high. HR loves you.
Testing Lottery-Based Code (Without Going Crazy)

"But wait," I hear you say. "How do I test code that's literally random?"
Great question! Laravel thought of this. The Lottery class comes with testing utilities that make randomness, not random.
Force a Win Every Time
use Illuminate\Support\Lottery;
public function test_new_dashboard_renders_for_lottery_winners()
{
Lottery::alwaysWin();
$response = $this->get('/dashboard');
$response->assertViewIs('dashboard.shiny-new-version');
}
Force a Loss Every Time
public function test_old_dashboard_renders_for_lottery_losers()
{
Lottery::alwaysLose();
$response = $this->get('/dashboard');
$response->assertViewIs('dashboard.boring-old-version');
}
Predetermine a Sequence
Need more control? You can script exactly what happens:
// First call wins, second loses, third wins, then random
Lottery::fix([true, false, true]);
Lottery::odds(1, 2)->choose(); // true (first in sequence)
Lottery::odds(1, 2)->choose(); // false (second in sequence)
Lottery::odds(1, 2)->choose(); // true (third in sequence)
Lottery::odds(1, 2)->choose(); // random (sequence exhausted)
Reset to Normal Behavior
Always clean up after yourself:
protected function tearDown(): void
{
Lottery::determineResultsNormally();
parent::tearDown();
}
This prevents test pollution. Nobody wants their tests failing because a previous test left the Lottery rigged.
Quick API Reference
Here's everything the Lottery class offers:
| Method | What It Does |
Lottery::odds($chances, $outOf) | Creates a new Lottery with the given probability |
->winner(callable $callback) | Sets the callback that runs when lottery wins |
->loser(callable $callback) | Sets the callback that runs when lottery loses |
->choose() | Executes the lottery and returns the result |
Lottery::alwaysWin() | Forces all lotteries to win (testing) |
Lottery::alwaysLose() | Forces all lotteries to lose (testing) |
Lottery::fix(array $sequence) | Sets a predetermined sequence of results |
Lottery::determineResultsNormally() | Resets to random behavior |
When NOT to Use Lottery
Let's be honest. The Lottery class isn't a silver bullet. Here's when you should reach for something else:
Feature Flags: If you need to know WHICH users saw WHAT, use Laravel Pennant or a proper feature flag system. Lottery is random per request, not per user.
A/B Testing with Analytics: Same deal. If you need conversion metrics, you need to track cohorts. Lottery won't remember who saw what.
Critical Error Reporting: If an error MUST be reported (payment failures, security issues), don't gamble with it. Some things are too important.
Rate Limiting: If you need "X requests per minute", use Laravel's rate limiter. Lottery is probabilistic, not deterministic.
The Elegance Factor
What I love about the Lottery class is how it transforms ugly code into something readable.
Before (the old way):
if (random_int(1, 100) <= 5) {
report($exception);
}
After (the Laravel way):
Lottery::odds(5, 100)
->winner(fn () => report($exception))
->choose();
The intent is crystal clear. Anyone reading this code immediately understands: "This reports 5% of exceptions."
And when you combine it with Laravel's callback-accepting methods:
DB::whenQueryingForLongerThan(
CarbonInterval::seconds(2),
Lottery::odds(1, 100)->winner(fn () => report('Slow query detected'))
);
No if statements. No manual random number generation. Just clean, fluent code that reads like English.
Wrapping Up

The Lottery class has been in Laravel since version 9.40, and it's still one of the framework's best-kept secrets. If you're dealing with high-volume events that need sampling, give it a try.
Next time you find yourself writing if (random_int(1, 100) <= 5), stop. Laravel has a better way.
Your error tracker will thank you. Your phone will thank you. And your 3 AM self will definitely thank you.
Found this helpful? Follow me for more Laravel tips and deep dives into features you probably didn't know existed.
Cheerio!





