spatie/laravel-long-running-tasks
Monitor externally executed long-running tasks in Laravel by polling instead of webhooks. Extend LongRunningTask, implement check() returning TaskResult::ContinueChecking or StopChecking, and start with MyTask::make()->meta($data)->start(). Checks run on a configurable interval until the task...
Architecture fit. This package is not a general-purpose job orchestrator; it is a narrow, opinionated state machine for one specific pattern: fire a request at an external service that will not call you back, then poll that service until it reports completion. It sits on top of Laravel's existing queue as the scheduler and a single Eloquent table (long_running_task_log_items) as the state store. That makes it a very good fit if our integration surface is AWS Rekognition, MediaConvert, document OCR/translation, model-training endpoints, or any vendor API whose completion signal is "GET status until done." It is a poor fit for anything that already emits webhooks, for in-process CPU-heavy work, or for multi-step pipelines with dependencies — it has no DAG, no fan-out/fan-in, no compensation logic.
Integration feasibility. Low friction by design: one composer require, one published migration, one published config, one subclass of LongRunningTask implementing check(). The entire developer contract is a single method returning an enum plus an optional onFail() hook. Per-task and per-instance overrides exist for interval, backoff strategy, queue, and timeout, so we can tune behaviour without forking. Requires an operational queue (already a prerequisite in most Laravel estates), so realistically an afternoon-to-one-day spike, not a project.
Technical risk. The code risk is low; the dependency risk is the concern. 38 stars, zero dependents, and a single vendor maintainer signal a package that is essentially unproven in the wild — Spatie maintains a large portfolio, but low-adoption packages get deprioritised when a Laravel major version lands. Additional risks: the default keep_checking_for_in_seconds of 300s will mark genuine long-running external tasks as didNotComplete unless we raise it on day one; polling volume against third-party APIs has direct cost and rate-limit implications; and the README has visible quality gaps (duplicated import in the example, an empty ### Events heading), which is a proxy for unknown observability hooks. Everything is MIT and the surface is small enough to vendor/fork if abandoned.
Key questions to resolve before committing.
long_running_task_log_items, or do we own table growth indefinitely?WithoutOverlapping or a row lock, and is check() required to be idempotent?attempt — is there a max-attempts ceiling, or does a permanently failing task loop until the timeout window closes?last_check_started_at?Stack fit. Requires a Laravel app with a working queue and a relational database (the state model assumes a meta JSON/array column). If we are already on Horizon or a managed queue with dead-letter handling, the dispatch/retry story is inherited rather than rebuilt. The custom-model and custom-job extension points mean we can add tenancy columns, cost-centre attribution, or a tenant_id scoping field to the log table without forking.
Migration path. Purely additive — no changes to existing models, no interception of existing jobs. The realistic pattern is to introduce it behind a domain-level abstraction: define our own Transcription/RenderJob service that owns the long-running task subclass, so call sites depend on our interface rather than a Spatie class. That single decision is what makes the package replaceable if it is abandoned.
Compatibility. Pin the version explicitly and validate against our Laravel/PHP matrix before adoption; the recent release date is encouraging but we should confirm the supported Laravel majors in composer.json rather than the README. Watch the config-driven log_model and task_job keys — both are global, so any future multi-tenant or multi-strategy split needs care.
Sequencing.
latest_exception/onFail behaviour.keep_checking_for_in_seconds to match real vendor SLAs, choose a backoff strategy per task class, define retention and add a scheduled prune, put the log table behind a read-only internal dashboard or Filament resource.didNotComplete rate for two weeks.Maintenance. Small code surface, MIT licence, and an extension model that lets us own customisations — but we should budget for the possibility of taking over maintenance. Practically: pin the version, keep our subclasses thin (all business logic in our own services), and schedule a quarterly review of upstream commits and Laravel compatibility. The empty Events section suggests we should not depend on upstream observability; build our own.
Support. Vendor support is effectively "GitHub issues" — no SLA. Internal ownership must be explicit: one team owns the log table, the retention job, the dashboards, and the alerting. The abstraction layer proposed above is the mitigation for a bus factor of one.
Scaling. Capacity planning is queue-throughput arithmetic and should be done up front. With the default 10-second interval, N concurrent tasks generate roughly N/10 check jobs per second — 1,000 in-flight tasks means ~100 jobs/sec of pure polling, which will dwarf normal application traffic on a shared queue. Levers: separate queue for polling jobs (the package supports per-task queue assignment), longer intervals where the vendor tolerates it, and backoff strategies (StandardBackoff stretches to 10, 60, 120, 300, 600s) which cut poll volume by an order of magnitude for slow vendors. The table will also grow at one row per task, so retention is a scaling concern, not just hygiene. Third-party rate limits and per-call costs are the binding constraint more often than our own infrastructure.
Failure modes to design for.
didNotComplete from the 300-second default — highest-probability production incident and easily avoided by explicit per-task timeouts.running but nothing is polling; requires a watchdog over last_check_started_at and alerting on age.latest_exception and the task stops unless onFail() returns ContinueChecking — without alerting, these are invisible failures discovered by users.Ramp-up. The developer learning curve is genuinely small — a Laravel engineer is productive in half a day, and the four backoff strategies are self-documenting in the config comments. The real ramp-up is operational, not technical: getting the team fluent in reading the log table as the system of record, defining what didNotComplete means for each workflow owner, and agreeing the on-call response when a task stalls. Budget that education alongside the technical rollout, and gate broad adoption on the pilot demonstrating both a healthy queue profile and a working alert path.
How can I help you explore Laravel packages today?