Don't Let Your Staging Server Die: Separate Task Scheduling in Laravel
Source: Dev.to
The Problem
If you’re building real apps, you need a staging environment. I run a simple one on a Digital Ocean droplet (2 vCPUs) for a Laravel 10 app. One day the server froze; htop showed both CPU cores at 100 % and memory nearly full:
1[||||||||||||||||||100.0%]
2[||||||||||||||||||100.0%]
Mem[||||||||||||||| 3.2G/4.0G]
Both CPUs were pegged and memory was hanging at 3.2 GB of 4 GB.
Why It Matters
A quick ps aux and log scan pointed to the scheduler. Heavy scheduled commands that make sense in production—price exports, supplier syncs with thousands of products, huge dataset crunching—were killing the smaller staging server. Staging doesn’t need those jobs running as often, if at all.
The Solution
Separate your schedule() method based on environment. In Laravel 10 you can edit app/Console/Kernel.php (Laravel 11+ moves this to routes/console.php).
final class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule): void
{
$this->scheduleCommon($schedule);
if ($this->app->environment(AppEnvironmentEnum::PRODUCTION->value)) {
$this->scheduleProduction($schedule);
}
if ($this->app->environment(AppEnvironmentEnum::STAGING->value)) {
// Less frequent or reduced workload for staging
$this->scheduleStaging($schedule);
}
}
}
How to Structure It
Common Schedule
Tasks that should run everywhere: cleanup, pruning, basic maintenance.
private function scheduleCommon(Schedule $schedule): void
{
// Keep DB lean everywhere
$schedule->command(PruneCommand::class, ['--model' => [CartItem::class]])->hourly();
$schedule->command(PruneCommand::class, ['--model' => [OrderItemNotification::class]])->daily();
// Clear expired tokens
$schedule->command(PruneExpired::class)->dailyAt('03:00');
// Basic notifications
$schedule->command(TelegramNotificationSendCommand::class)->hourly()->withoutOverlapping();
$schedule->command(UserSendConfirmationNotificationCommand::class)->hourly();
}
Production Schedule
Heavy operations that need frequent execution and have proper resources.
private function scheduleProduction(Schedule $schedule): void
{
// Search index sync
$schedule->command(SearchIndexSyncDiff::class)->dailyAt('05:00');
// Auto price exports - multiple times per day
$schedule->command(PriceExportAutoDispatchCommand::class)
->cron('15 6,8,10,12,14,16,18 * * *');
// Supplier sync - runs 7 times per day
$schedule->command(TmSyncPriceExportProduct::class)
->cron('1 6,8,10,12,14,16,18 * * *');
// Product availability updates
$schedule->command(ProductAvailabilityUpdateCommand::class)
->hourlyAt(15)
->between('08:00', '19:00');
}
Staging Schedule
Same commands, but less frequent—only what you actually need for testing.
private function scheduleStaging(Schedule $schedule): void
{
// Price exports - once per day is enough
$schedule->command(PriceExportAutoDispatchCommand::class)
->cron('15 6 * * *');
// Supplier sync - once per day
$schedule->command(TmSyncPriceExportProduct::class)
->cron('1 6 * * *');
// Test suspended orders more frequently for debugging
$schedule->command(OrderProcessSuspendedCommand::class)
->everyMinute()
->between('08:00', '19:00')
->withoutOverlapping();
}
The Results
After splitting the workloads, the staging server’s CPU dropped to 15‑20 % and memory usage stabilized around 1.5 GB.
| Environment | CPU Usage | Memory Usage |
|---|---|---|
| Staging (before) | 100 % | 3.2 GB / 4 GB |
| Staging (after) | 15‑20 % | 1.5 GB / 4 GB |
Production continued operating at full capacity without impact.
Conclusions
Separate task schedules by environment from day one. Let staging run only essential maintenance routines and throttle heavy jobs. Copying production schedules blindly to staging quickly leads to resource exhaustion. Adjust frequencies to match your server resources, and your infrastructure will stay healthy.