ranjbarali / laravel-queue-quarantine
A resilient quarantine and diagnostics layer for Laravel queue failures that cannot be safely persisted by the normal failed-job system.
Package info
github.com/RanjbarAli/laravel-queue-quarantine
pkg:composer/ranjbarali/laravel-queue-quarantine
Requires
- php: ^8.2
- illuminate/console: ^12.0 || ^13.0
- illuminate/contracts: ^12.0 || ^13.0
- illuminate/database: ^12.0 || ^13.0
- illuminate/encryption: ^12.0 || ^13.0
- illuminate/filesystem: ^12.0 || ^13.0
- illuminate/queue: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
Requires (Dev)
- larastan/larastan: ^3.0 || ^4.0
- laravel/pint: ^1.24
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0 || ^11.0
- phpunit/phpunit: ^11.5 || ^12.0 || ^13.0
This package is auto-updated.
Last update: 2026-08-26 23:56:59 UTC
README
Laravel Queue Quarantine
Never lose a Laravel queue failure just because its payload is corrupted.
A resilient fallback and diagnostic layer for failures that Laravel's normal failed-job provider cannot safely preserve.
Important
Queue Quarantine does not replace Laravel's failed_jobs system. It activates only when Laravel's configured failed-job provider throws while trying to persist a failure.
Table of contents
- Why this package exists
- How it works
- Requirements
- Installation
- Quick start
- Demo: investigating a corrupted job
- Configuration
- Artisan command reference
- Incident classifications
- Retry safety
- Programmatic API
- Custom classifiers
- Storage drivers
- Security and privacy
- Events and observability
- Scheduling automatic pruning
- Architecture and limitations
- Testing and development
- Contributing and security
Why this package exists
Laravel normally records a failed job through its configured failed-job provider. That record lets an operator inspect or retry the job later. Some providers must parse the queue payload before storing it. If the payload itself is malformed—or the provider is unavailable—the safety mechanism can fail too.
Typical causes include:
- malformed or truncated JSON;
- invalid UTF-8 bytes;
- a missing or invalid UUID;
- an unknown job class;
- corrupted serialized command data;
- decryption or unserialization failures;
- database, filesystem, or external failed-provider errors;
- payloads larger than an application's safe retention limit.
Queue Quarantine preserves the evidence and diagnostic metadata without modifying Laravel's worker, replacing normal failed jobs, or duplicating Horizon.
How it works
Queue job fails
│
▼
Laravel failed-job provider
│
├── succeeds ────────────────► Laravel failed_jobs ───► done
│
└── throws
│
▼
Queue Quarantine
│
classify + redact
│
┌──────┴──────┐
▼ ▼
Database File store
│ │
└──────┬──────┘
│ store also fails
▼
Minimal emergency record
(metadata and hash only)
The package decorates Laravel's public queue.failer service and delegates every call to the original provider first. Capture starts only when FailedJobProviderInterface::log() throws.
This gives four useful guarantees:
- Successful Laravel failed-job persistence is unchanged.
- Original bytes are available before diagnostic parsing.
- Quarantine storage failures cannot create a recursive failure loop.
- No Laravel framework files, worker classes, or queue payloads are patched.
Requirements
| Laravel | Supported PHP |
|---|---|
| 12.x | 8.2–8.5 |
| 13.x | 8.3–8.5 |
The database driver requires a Laravel-supported database connection. The file driver requires a writable application storage directory.
Installation
1. Install the package
Add Queue Quarantine to an existing Laravel application:
composer require ranjbarali/laravel-queue-quarantine
Laravel package discovery registers the provider and facade automatically.
2. Publish configuration
Publish the configuration when you need to change storage, security, classification, or retention settings:
php artisan vendor:publish --tag=queue-quarantine-config
This creates config/queue-quarantine.php in the host application.
3. Prepare database storage
The database driver is the production default. Publish its migration and create the quarantine table:
php artisan vendor:publish --tag=queue-quarantine-migrations php artisan migrate
If you exclusively use QUEUE_QUARANTINE_DRIVER=file, the migration is not required.
4. Restart long-running workers
Make workers reload the application container so the decorator becomes active:
php artisan queue:restart
Laravel asks workers to exit gracefully after their current job. Your process manager should start fresh workers automatically.
Quick start
Continue running workers normally—there is no replacement worker command:
php artisan queue:work
If Laravel stores a failed job successfully, quarantine remains empty. If the failed-job provider throws, list preserved incidents:
php artisan queue:quarantine
+-------------------------------+---------+-------------------+-----------+
| Incident | Queue | Classification | Age |
+-------------------------------+---------+-------------------+-----------+
| qnt_01K3PD7MNVF29Z1CPQ23WFE6K | reports | TRUNCATED_PAYLOAD | 0d 00h 2m |
| qnt_01K3PCYQ8GAM7B6X2F9V4R9ST | emails | INVALID_UTF8 | 0d 00h 8m |
+-------------------------------+---------+-------------------+-----------+
Demo: investigating a corrupted job
Suppose a Redis payload was truncated. Laravel emits JobFailed, but its UUID-based failed-job provider cannot persist the malformed body. Queue Quarantine preserves it as qnt_01K3PD7MNVF29Z1CPQ23WFE6K.
1. Find the incident
Filter by queue and a recent time window:
php artisan queue:quarantine --queue=reports --since="24 hours ago"
Add --classification=TRUNCATED_PAYLOAD when the list contains multiple failure types.
2. Review safe metadata
Show a summary without exposing raw payload or exception trace:
php artisan queue:quarantine:show qnt_01K3PD7MNVF29Z1CPQ23WFE6K
ID ................................ qnt_01K3PD7MNVF29Z1CPQ23WFE6K
Connection ........................ redis
Queue ............................. reports
Classification .................... TRUNCATED_PAYLOAD
Detected job ...................... App\Jobs\GenerateReport
Payload size ...................... 8,509 bytes
JSON error ........................ Syntax error
Laravel ........................... 13.25.0
PHP ............................... 8.4.12
Captured .......................... 2026-08-27 03:02:18 +04:00
Retry safety ...................... UNSAFE
3. Run deterministic diagnostics
Inspect structural evidence without unserializing the command:
php artisan queue:quarantine:inspect qnt_01K3PD7MNVF29Z1CPQ23WFE6K
✓ UUID detected
✓ Job class detected
✗ JSON valid
✗ Serialized command present
Last valid JSON path .............. data.command
Possible cause .................... The payload may have been truncated before completion.
Retry safety ...................... UNSAFE
Diagnostics say “possible cause” when evidence is not conclusive.
4. Export evidence safely
Create a default metadata-only JSON export for a private incident report:
php artisan queue:quarantine:export \
qnt_01K3PD7MNVF29Z1CPQ23WFE6K \
--output=incident.json
The default export omits raw_payload and exception_trace. Include them only in a controlled environment:
php artisan queue:quarantine:export \
qnt_01K3PD7MNVF29Z1CPQ23WFE6K \
--output=incident-sensitive.json \
--raw
5. Make a recovery decision
Truncated payloads are unsafe, so automatic retry is refused—even with --force:
php artisan queue:quarantine:retry qnt_01K3PD7MNVF29Z1CPQ23WFE6K
This payload cannot be safely retried automatically.
Repair the source of corruption and dispatch a reconstructed application job instead of replaying unsafe bytes.
Configuration
The published configuration includes these controls:
return [ 'enabled' => env('QUEUE_QUARANTINE_ENABLED', true), 'driver' => env('QUEUE_QUARANTINE_DRIVER', 'database'), 'capture_raw_payload' => env('QUEUE_QUARANTINE_CAPTURE_PAYLOAD', true), 'capture_exception_trace' => env('QUEUE_QUARANTINE_CAPTURE_TRACE', true), 'max_payload_size' => 2 * 1024 * 1024, 'classification' => true, 'encrypt' => env('QUEUE_QUARANTINE_ENCRYPT', false), 'database' => [ 'connection' => env('QUEUE_QUARANTINE_DB_CONNECTION'), 'table' => 'queue_quarantine_incidents', ], 'file' => [ 'path' => storage_path('app/queue-quarantine/incidents'), ], 'retention' => ['days' => 30], 'release' => env('APP_RELEASE'), 'redact' => [ 'password', 'password_confirmation', 'token', 'access_token', 'refresh_token', 'authorization', 'api_key', 'secret', ], 'classifiers' => [], ];
Recommended production environment values:
QUEUE_QUARANTINE_ENABLED=true QUEUE_QUARANTINE_DRIVER=database QUEUE_QUARANTINE_CAPTURE_PAYLOAD=true QUEUE_QUARANTINE_CAPTURE_TRACE=true QUEUE_QUARANTINE_ENCRYPT=true QUEUE_QUARANTINE_DB_CONNECTION=mysql APP_RELEASE=2026.08.27-1
| Setting | Purpose |
|---|---|
enabled |
Enables the decorator. Disable it to restore Laravel's failed provider unchanged. |
driver |
Selects the built-in database or file store. |
capture_raw_payload |
Controls payload retention. Hash and byte size are still recorded when disabled. |
capture_exception_trace |
Controls failed-provider trace storage. |
max_payload_size |
Prevents unexpectedly large bodies from being copied into storage. |
classification |
Enables built-in and custom classifiers. |
encrypt |
Encrypts payload and trace fields using Laravel's encrypter and stable APP_KEY. |
release |
Adds a deployment identifier to every incident. |
redact |
Lists case-insensitive JSON keys replaced with [REDACTED]. |
Warning
Field-level redaction works only when JSON can be decoded. Completely malformed payloads are opaque bytes. Disable raw capture if retaining such data conflicts with your security policy.
Artisan command reference
List incidents
php artisan queue:quarantine
Combine filters to focus an investigation:
# Incidents from one queue. php artisan queue:quarantine --queue=reports # One classification only. php artisan queue:quarantine --classification=INVALID_JSON # Incidents captured recently. php artisan queue:quarantine --since="24 hours ago" # Combine all supported filters. php artisan queue:quarantine \ --connection=redis \ --queue=reports \ --classification=TRUNCATED_PAYLOAD \ --since="7 days ago"
Show an incident
php artisan queue:quarantine:show <incident-id>
Displays routing, classification, runtime, size, capture time, and retry-safety metadata. Raw content is never printed.
Diagnose a payload
php artisan queue:quarantine:inspect <incident-id>
Checks JSON, UUID, detected job class, and serialized command presence without executing payload code.
Retry an incident
php artisan queue:quarantine:retry <incident-id>
Only retriable incidents can return to their original connection and queue. REQUIRES_CONFIRMATION prompts interactively. Automation can acknowledge the prompt with:
php artisan queue:quarantine:retry <incident-id> --force
--force bypasses confirmation; it never overrides UNSAFE.
Export an incident
# Print safe JSON to stdout. php artisan queue:quarantine:export <incident-id> # Write safe JSON to a file. php artisan queue:quarantine:export <incident-id> --output=incident.json # Explicitly include stored payload and trace. php artisan queue:quarantine:export <incident-id> --output=incident-sensitive.json --raw
JSON is the current export format. Default exports omit sensitive body and trace fields.
Prune expired incidents
# Use configured retention.days. php artisan queue:quarantine:prune # Override retention for this run. php artisan queue:quarantine:prune --days=7
The command reports the deletion count and rejects values below one day.
Incident classifications
| Classification | Meaning | Retry safety |
|---|---|---|
INVALID_JSON |
Invalid JSON without strong truncation evidence. | UNSAFE |
TRUNCATED_PAYLOAD |
Delimiters or parser state indicate an incomplete body. | UNSAFE |
INVALID_UTF8 |
Payload bytes are not valid UTF-8. | UNSAFE |
MISSING_UUID |
Parsed payload lacks a usable UUID. | UNSAFE |
INVALID_UUID |
UUID exists but is invalid. | UNSAFE |
UNKNOWN_JOB_CLASS |
A class-shaped job name cannot be resolved. | UNSAFE |
UNSERIALIZE_FAILURE |
Serialized command restoration failed. | UNSAFE |
DECRYPT_FAILURE |
Decryption or MAC validation failed. | UNSAFE |
FAILED_JOB_PROVIDER_FAILURE |
Payload is structurally valid, but persistence threw. | REQUIRES_CONFIRMATION if retained |
PAYLOAD_TOO_LARGE |
Body exceeds max_payload_size and is not retained. |
UNSAFE |
UNKNOWN |
Classification is disabled or no result was produced. | UNSAFE |
Classification is diagnostic—not proof of root cause. Review infrastructure, deployment events, provider health, and exception metadata together.
Retry safety
SAFE: reserved for payloads validated without ambiguity.REQUIRES_CONFIRMATION: structurally valid retained payload; an operator must review the provider error.UNSAFE: replay is prohibited, including with--force.
A successful retry emits QuarantineIncidentRetried and retains the incident as audit evidence. Delete or prune it separately after confirming recovery.
Programmatic API
use RanjbarAli\QueueQuarantine\Facades\QueueQuarantine; $incident = QueueQuarantine::find('qnt_01K3PD7MNVF29Z1CPQ23WFE6K'); if ($incident !== null) { logger()->info('Quarantined queue incident found', [ 'incident_id' => $incident->incidentId, 'classification' => $incident->classification->value, 'payload_hash' => $incident->payloadHash, 'retry_safety' => $incident->retrySafety()->value, ]); } $reports = QueueQuarantine::all([ 'connection' => 'redis', 'queue' => 'reports', 'classification' => 'TRUNCATED_PAYLOAD', 'since' => now()->subDay()->toDateTimeString(), ]); foreach ($reports as $report) { // Forward metadata—not raw payloads—to an incident system. } QueueQuarantine::delete('qnt_01K3PD7MNVF29Z1CPQ23WFE6K');
The common filter API behaves consistently across database and file storage. It intentionally does not expose a database-only fake ORM.
Custom classifiers
Implement PayloadClassifier for application-specific structured evidence:
<?php namespace App\Queue; use RanjbarAli\QueueQuarantine\Classification\Classification; use RanjbarAli\QueueQuarantine\Classification\PayloadClassifier; use Throwable; final class SignedPayloadClassifier implements PayloadClassifier { public function classify(string $payload, Throwable $exception): ?Classification { if (! str_contains($exception->getMessage(), 'signature mismatch')) { return null; // Let the next classifier inspect the incident. } return Classification::DecryptFailure; } }
Register it when workers boot:
'classifiers' => [ App\Queue\SignedPayloadClassifier::class, ],
Or extend classification during application boot:
use RanjbarAli\QueueQuarantine\Facades\QueueQuarantine; QueueQuarantine::extendClassifier(App\Queue\SignedPayloadClassifier::class);
Custom classifiers run before the default classifier. Return null for unknown incidents, avoid unserialization, and prefer structured evidence over message matching.
Storage drivers
Database
The production default provides indexed filtering. A separate connection can keep quarantine available during primary database failure:
QUEUE_QUARANTINE_DRIVER=database QUEUE_QUARANTINE_DB_CONNECTION=quarantine
Define that connection normally in config/database.php.
File
The file driver stores one JSON document per incident under storage/app/queue-quarantine/incidents. Writes use a temporary file and atomic rename:
QUEUE_QUARANTINE_DRIVER=file
Use durable storage and backups if these files are operational records.
Custom storage
Implement QuarantineStore and bind it in an application service provider:
use App\Queue\S3QuarantineStore; use RanjbarAli\QueueQuarantine\Contracts\QuarantineStore; $this->app->singleton(QuarantineStore::class, S3QuarantineStore::class);
Custom stores should make writes atomic, preserve ULIDs, honor encryption policy, and throw on failure. The package then attempts its terminal emergency record.
Security and privacy
Treat quarantine storage as production-sensitive data.
Redaction
For valid JSON, configured keys are matched case-insensitively at every nesting level:
{
"email": "person@example.com",
"token": "[REDACTED]",
"nested": {"authorization": "[REDACTED]"}
}
Add domain-specific names such as private_key or session_id to the redaction list.
Encryption at rest
Encrypt payload and trace fields through Laravel's encrypter:
QUEUE_QUARANTINE_ENCRYPT=true
Keep APP_KEY stable during the retention period. Key rotation requires re-encrypting or pruning records protected with the old key.
Emergency fallback
If quarantine storage throws, the final local record includes only timestamp, connection, queue, payload size, SHA-256 hash, and exception class names. It excludes payload bytes, messages, and traces. Failure of this final write is suppressed so quarantine cannot recursively crash a worker.
Production checklist
- Restrict table, directory, backup, and operator access.
- Disable raw capture where opaque malformed data cannot legally be retained.
- Never attach
--rawexports to public issues or general chat systems. - Monitor
QuarantineCaptureFailed. - Never log
$incident->rawPayloadin ordinary application logs.
Events and observability
| Event | Meaning |
|---|---|
QuarantineIncidentCaptured |
A store successfully preserved an incident. |
QuarantineIncidentDeleted |
An incident was explicitly deleted. |
QuarantineIncidentRetried |
A retained payload was dispatched. |
QuarantineCaptureFailed |
Quarantine storage threw and fallback was attempted. |
Example listener:
use Illuminate\Support\Facades\Event; use RanjbarAli\QueueQuarantine\Events\QuarantineIncidentCaptured; Event::listen(QuarantineIncidentCaptured::class, function ($event): void { logger()->warning('Queue failure quarantined', [ 'incident_id' => $event->incident->incidentId, 'classification' => $event->incident->classification->value, 'connection' => $event->incident->connection, 'queue' => $event->incident->queue, ]); });
Forward identifiers, classifications, hashes, and routing metadata—not raw payloads—to monitoring systems.
Scheduling automatic pruning
Schedule daily pruning in routes/console.php:
use Illuminate\Support\Facades\Schedule; Schedule::command('queue:quarantine:prune') ->dailyAt('02:30') ->withoutOverlapping() ->onOneServer();
Verify the application's scheduler configuration:
php artisan schedule:list
Architecture and limitations
See docs/architecture.md for the integration boundary and extension model.
- The package captures only payloads delivered to a worker that reaches failed-job logging.
- It cannot recover a message lost inside a queue backend before delivery.
- It quarantines failed-provider exceptions, not every ordinary failed job.
- Classification is conservative and may report
UNKNOWN. - Unsafe payloads must be repaired or reconstructed, not replayed.
- This package provides commands and APIs, not a dashboard.
Testing and development
Install development dependencies:
composer install
Run the complete release gate:
composer check
It runs:
composer validate --strict Validate package metadata and constraints
composer format:test Check Pint formatting without changing files
composer analyse Run Larastan/PHPStan static analysis
composer test Run PHPUnit and Orchestra Testbench
Individual development commands:
composer format # Automatically format PHP files. composer analyse # Run static analysis only. composer test # Run automated tests only.
GitHub Actions covers Laravel 12 and 13 across supported PHP versions. The suite covers provider decoration, containment, classification, redaction, both stores, commands, exports, pruning, and retry safety.
Contributing and security
Read CONTRIBUTING.md before opening a pull request. Potential vulnerabilities must not be reported publicly; follow SECURITY.md, especially for payload disclosure, redaction, encryption, or unsafe retry findings.
License
Laravel Queue Quarantine is open-source software released under the MIT License.