arxeiss/sansdaemon
Sansdaemon lets Laravel apps process queued jobs without a long-running worker daemon. Trigger the worker from cron or the scheduler instead of Supervisor, making queue processing simpler on shared hosting and small deployments.
arxeiss/sansdaemon solves a narrow, real problem: running Laravel queue workers on hosts where you cannot keep a long-lived queue:work process alive (no Supervisor, no systemd, shared hosting, small single-box deployments). Instead of a daemon, the package gives you an artisan command that drains the queue and exits, so it can be driven by the scheduler / cron.
Minimal path to a first working setup:
composer require arxeiss/sansdaemonphp artisan vendor:publish --tag=sansdaemon-config or the tag listed in the README — check the README, tags vary by version).routes/console.php / app/Console/Kernel.php:Schedule::command('sansdaemon:work')
->everyMinute()
->withoutOverlapping()
->runInBackground();
php artisan schedule:run every minute (cron entry or Schedule::command('schedule:run')->everyMinute() equivalent on the platform).Where to look first: the package's console command signature (php artisan list | grep -i sans) and the published config file. Those two tell you the connection/queue selection options, the max-runtime knob, and whether it wraps queue:work --stop-when-empty or implements its own loop. Read those before the README, which is thin.
First real use case: a low-to-moderate volume app (mail, webhooks, PDF generation a few thousand times a day) on a single box, where a per-minute drain is indistinguishable from a daemon but survives deploys and OOM kills without Supervisor babysitting.
Scheduler-driven drain (the default pattern). One scheduled entry per queue/connection you care about. Keep withoutOverlapping() always — a slow drain plus a one-minute cron is exactly how you get two workers on the same queue and duplicated side effects on non-idempotent jobs.
Schedule::command('sansdaemon:work --queue=default')
->everyMinute()->withoutOverlapping()->runInBackground();
Schedule::command('sansdaemon:work --queue=emails,webhooks')
->everyMinute()->withoutOverlapping(10)->runInBackground();
Multi-queue priority. Pass a comma-separated queue list so low-priority work (reports) only runs after high-priority work (password resets) is drained. Order is priority order; it is not round-robin.
Environment split. Typical pattern is daemon-less in staging/shared hosting, real queue:work under Supervisor in production:
if (app()->environment('production') === false) {
Schedule::command('sansdaemon:work')->everyMinute()->withoutOverlapping();
}
Keep the condition explicit rather than hiding it in config, so nobody wonders why production queues behave differently.
Deploy integration. Short-lived workers pick up new code on the next tick automatically — you do not need queue:restart for these. Do keep php artisan config:cache/route:cache out of a half-deployed state, and avoid long-running jobs that straddle a deploy. If you do also run a Supervisor daemon, queue:restart is still required for that one.
Bounding the run. Treat the scheduler interval as your hard budget. If ticks are every 60s, configure the worker's max runtime to ~50s (config key or --max-time, whatever the command exposes) so a backlog cannot cause overlapping invocations that withoutOverlapping will then silently skip.
Pairing with onOneServer() when you have more than one app server: without it, every box drains the same queue. Either use onOneServer() (needs a shared cache driver — Redis/database, not array/file) or run the worker on exactly one designated host.
Local dev. Don't schedule it locally; just run the command in a terminal loop while developing so you see output immediately. Reserve the scheduled form for environments that mimic production cron.
--max-time/timeout collide. A job that runs longer than the worker budget gets terminated mid-flight; depending on the timeout configuration it is released back and retried, which can look like a duplicate or a mystery MaxAttemptsExceededException. Set $timeout on the job and keep it comfortably under the worker budget, and prefer chunked/self-requeueing jobs over monoliths.--stop-when-empty semantics. If the command wraps this flag, a burst that arrives one second after the worker exits waits a full interval. That is expected, not a bug — but it makes the tool unsuitable for latency-sensitive pipelines.withoutOverlapping needs a cache lock that actually persists. With CACHE_STORE=array or a per-process store, the mutex is useless and you get concurrent drains. Same story for onOneServer(). Verify with Redis, database, or memcached.withoutOverlapping logs nothing by default when it skips. If queues appear "stuck," check whether the previous tick is still running (ps aux | grep sans) before assuming the package is broken.runInBackground() requires proc_open. Disabled on some hardened shared hosts; the schedule will fail with an opaque error. Test the exact production PHP binary, not your local one, and remember the scheduler uses the PHP binary it was invoked with.$_ENV or shell variables can behave differently under cron than in an SSH session — always config:cache and read via config().ShouldBeUnique, WithoutOverlapping middleware, or database uniqueness constraints on jobs dispatched from user actions.-v; check storage/logs/laravel.log for the scheduler's own output, and remember background task output is not echoed to your terminal. php artisan schedule:list confirms the entry exists with the expected expression.php artisan config:clear after changes in non-production.--ignore-platform-req hack or a fork. Pin ^1.3.2 if you are moving to Laravel 13, and re-check the published config after minor bumps in case new keys were added (unpublished defaults still apply, but explicit config files drift).How can I help you explore Laravel packages today?