Een webshop draait niet alleen tijdens kantooruren. Voorraad verschuift, feeds verlopen, betalingen komen binnen en logs lopen vol.
Laravel scheduler voor e-commerce: dagelijkse jobs die je nodig hebt
Een webshop draait niet alleen tijdens kantooruren. Voorraad verschuift, feeds verlopen, betalingen komen binnen en logs lopen vol. Wij zien bij klanten dat 80% van de operationele problemen ontstaat door taken die niemand automatiseerde. De Laravel scheduler lost dat op met één cron-entry en code die je versiebeheert.
Geen losse crontab-regels per server. Geen vergeten scripts op een productiemachine. Eén in cron, en al je terugkerende werk staat in routes/console.php of je Schedule-definitie. Dit artikel laat de concrete jobs zien die elke webshop nodig heeft, met code-patroon en monitoring.
Eén cron-entry, alles in code
De Laravel scheduler werkt met precies één crontab-regel:
* * * * * cd /home/forge/shop.nl && php artisan schedule:run >> /dev/null 2>&1
Die regel draait elke minuut. Laravel beslist daarna zelf wat er moet draaien. Het voordeel: je hele planning staat in je codebase, gaat mee in git, en is identiek op staging en productie.
In Laravel 11+ definieer je taken in routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('inventory:sync')
->everyFifteenMinutes()
->withoutOverlapping()
->runInBackground();
Drie methodes die je bij e-commerce bijna altijd nodig hebt:
withoutOverlapping()— voorkomt dat een trage sync zichzelf inhaalt. Cruciaal bij voorraad.runInBackground()— laat lange taken parallel draaien zodat ze elkaar niet blokkeren.onOneServer()— bij meerdere app-servers draait de taak maar één keer.
Wij bouwen schedulers altijd zo dat geen enkele taak afhankelijk is van de volgorde waarin cron ze aanroept. Idempotent en herhaalbaar. Wil je weten hoe wij dit inrichten in Laravel maatwerk? Dan praten we graag door over jouw situatie.
Voorraadsync: het hart van elke shop
Verkochte voorraad die niet wordt teruggekoppeld naar je ERP, of ERP-voorraad die niet binnenkomt in je shop, kost direct omzet. Of erger: oververkoop en geannuleerde orders.
Een voorraadsync hoeft niet realtime te zijn. Voor de meeste shops is elke 15 minuten ruim voldoende:
Schedule::command('inventory:sync')
->everyFifteenMinutes()
->withoutOverlapping(10)
->onOneServer()
->appendOutputTo(storage_path('logs/inventory-sync.log'));
De withoutOverlapping(10) zet een lock van 10 minuten. Crasht de taak hard, dan loopt de lock vanzelf af en blokkeert hij je shop niet permanent.
Belangrijk: laat de scheduler alleen inplannen en wegzetten in de queue. Het echte synchroniseren hoort in een job. Zo blijft de scheduler snel en schaalt het werk apart. Meer hierover lees je in onze post over Laravel queues voor orderverwerking.
class SyncInventory extends Command
{
protected $signature = 'inventory:sync';
public function handle(): int
{
Product::query()
->where('sync_enabled', true)
->chunkById(500, function ($products) {
foreach ($products as $product) {
SyncProductStock::dispatch($product);
}
});
return self::SUCCESS;
}
}
chunkById houdt je geheugengebruik laag, ook bij 200.000 producten. De daadwerkelijke API-call naar je ERP gebeurt in SyncProductStock, met retries en backoff.
Feed-export: shopping, marketplaces, affiliates
Google Shopping, Bol.com, Channable, affiliate-netwerken: ze willen allemaal een verse productfeed. Een feed die 's nachts faalt en niemand het merkt, betekent een dag lang afgekeurde advertenties.
Genereer feeds buiten piekuren. Een grote XML-feed kan minuten kosten en je database belasten:
Schedule::command('feed:generate google-shopping')
->dailyAt('04:00')
->withoutOverlapping()
->runInBackground()
->emailOutputOnFailure('[email protected]');
Patronen die wij hanteren bij feeds:
- Schrijf naar een temp-bestand, hernoem daarna atomair naar de live URL. Zo serveer je nooit een half geschreven feed.
- Houd alleen actieve, voorradige producten in performance-gerichte feeds. Minder ruis, betere ROAS.
- Log het aantal regels per export. Een feed die plots van 40.000 naar 3.000 producten zakt, is een alarmsignaal.
- Cache-warming na export, zodat de eerste crawler geen koude pagina raakt.
Een feed van 50.000 producten draaien we vaak in batches via de queue, niet als één synchroon script. De scheduler trapt het proces af, de workers doen het zware werk.
Opschonen: voorkom dat je shop dichtslibt
Een webshop produceert dagelijks afval. Verlaten winkelwagens, verlopen sessies, oude logs, tijdelijke export-bestanden, soft-deleted records. Zonder opschoning groeit je database tot queries traag worden en je schijf vol loopt.
Een paar concrete opruim-jobs:
| Taak | Frequentie | Wat |
|---|---|---|
| Verlaten carts | dagelijks 03:00 | Verwijder carts ouder dan 30 dagen |
| Sessies | dagelijks | php artisan session:gc of eigen cleanup |
| Logbestanden | wekelijks | Roteer en comprimeer oude logs |
| Temp exports | dagelijks | Verwijder feeds/CSV's ouder dan 7 dagen |
| Failed jobs | dagelijks | Archiveer en alarmeer bij ophoping |
Schedule::command('cleanup:abandoned-carts --days=30')
->dailyAt('03:00');
Schedule::command('cleanup:temp-exports --hours=168')
->dailyAt('03:15');
Schedule::command('queue:prune-failed --hours=336')
->daily();
Verlaten carts zijn ook een marketing-kans. Voordat je ze verwijdert, kun je een herstel-mail triggeren. De scheduler is de logische plek om die flow te starten: selecteer carts tussen 1 en 24 uur oud, dispatch een mail-job, markeer als benaderd.
Rapportages: cijfers in je inbox, niet in een dashboard dat niemand opent
Dagelijkse omzet, top-producten, voorraad onder de drempel, mislukte betalingen. Wij zien dat ondernemers dashboards zelden openen, maar een korte mail om 08:00 wél lezen.
Schedule::command('report:daily-sales')
->dailyAt('07:00')
->timezone('Europe/Amsterdam');
Schedule::command('report:low-stock --threshold=5')
->weekdays()
->dailyAt('07:30');
Let op de timezone(). Standaard draait de scheduler in UTC. Voor een Nederlandse shop wil je rapportages op lokale tijd, anders ontvang je in de winter een rapport om 08:00 dat eigenlijk over 07:00-data gaat.
Houd rapporten kort en actiegericht. Geen vanity metrics. Wat moet de ondernemer vandaag doen? Drie producten zijn bijna uitverkocht. Twaalf betalingen mislukten gisteren. Dat is bruikbaar.
Betaalreconciliatie: de stille killer
Dit is de job die het vaakst ontbreekt en de meeste schade aanricht. Je payment provider (Mollie, Adyen, Stripe) en je shop-administratie lopen onvermijdelijk uit de pas. Webhooks missen. Statussen blijven hangen op pending. Refunds komen niet door.
Een dagelijkse reconciliatie haalt alle transacties van de provider op en vergelijkt ze met je orders:
Schedule::command('payments:reconcile --date=yesterday')
->dailyAt('06:00')
->withoutOverlapping()
->onOneServer()
->emailOutputOnFailure('[email protected]');
Wat zo'n job concreet controleert:
- Orders
pendingouder dan 2 uur terwijl de providerpaidmeldt — webhook gemist. - Betalingen bij de provider zonder bijbehorende order — datavervuiling of fraude.
- Bedragverschillen tussen order-totaal en daadwerkelijk afgeschreven bedrag.
- Refunds die wel bij de provider staan maar niet in je shop.
Elk verschil log je en rapporteer je. De meeste shops draaien hier jarenlang zonder, en ontdekken pas bij de jaarafsluiting dat duizenden euro's niet kloppen. Een reconciliatie-job van een paar uur bouwwerk voorkomt dat. Wil je dit laten inrichten op je Magento- of Laravel-backend? Neem contact op, dan kijken we mee naar je betaalflow.
Monitoring: een job die stil faalt, is geen job
Het gevaarlijkste aan geplande taken: ze falen geluidloos. De shop draait door, niemand merkt het, totdat het te laat is. Monitoring is daarom geen extra, maar onderdeel van de taak zelf.
Drie lagen die wij standaard inbouwen:
1. Failure-notificaties. Laravel kan output bij falen mailen of pingen:Schedule::command('payments:reconcile')
->dailyAt('06:00')
->emailOutputOnFailure('[email protected]')
->onFailure(function () {
// log naar Sentry, Slack, etc.
});
Schedule::command('inventory:sync')
->everyFifteenMinutes()
->pingOnSuccess('https://ohdear.app/checks/abc/ping');
Test je planning lokaal zonder te wachten op de klok:
php artisan schedule:test
php artisan schedule:list
schedule:list toont al je taken met hun cron-expressie en eerstvolgende run. Onmisbaar bij het debuggen van "waarom draait dit niet op het juiste moment".
Veelgestelde vragen
Heb ik voor de Laravel scheduler meerdere cron-jobs nodig?
Nee. Je hebt precies één crontab-regel nodig die elke minuut php artisan schedule:run aanroept. Alle planning daarna staat in je Laravel-code. Dat is het hele punt: één entry op de server, al je taken in git.
Moet voorraadsync realtime zijn?
Voor de meeste shops niet. Elke 15 minuten volstaat ruimschoots en bespaart je veel API-calls richting je ERP. Realtime sync via webhooks is alleen nodig bij zeer schaarse voorraad of producten die in minuten uitverkopen. Voor de rest is een geplande sync stabieler en goedkoper.
Wat is het verschil tussen de scheduler en queues?
De scheduler bepaalt wanneer iets gebeurt (de timing). Queues bepalen hoe* het zware werk wordt uitgevoerd (de verwerking). In de praktijk combineer je ze: de scheduler trapt af en zet jobs in de queue, de workers doen het rekenwerk. Meer hierover in onze post over Laravel queues.
Wat als een geplande taak langer duurt dan het interval?
Gebruik withoutOverlapping(). Dan slaat de scheduler de volgende run over zolang de vorige nog loopt. Zonder die methode kunnen taken zich opstapelen, vooral bij voorraadsync. Geef altijd een lock-timeout mee, zodat een gecrashte taak je shop niet permanent blokkeert.
Hoe weet ik of mijn geplande taken daadwerkelijk draaien?
Combineer drie dingen: failure-notificaties via mail of Slack, externe heartbeat-monitoring (Oh Dear, Healthchecks.io) en health-checks ín de taak zelf. Vertrouw nooit op "geen nieuws is goed nieuws". Een job die stil faalt, ontdek je anders pas als de schade al is aangericht. Wil je je scheduler laten doorlichten? Neem contact op.

Geschreven door Ruthger Idema
15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.
Meer over ons team →