Looking to hire Laravel developers? Try LaraJobs

laravel-queue-quarantine maintained by ranjbarali

Description
A resilient quarantine and diagnostics layer for Laravel queue failures that cannot be safely persisted by the normal failed-job system.
Author
Ali Ranjbar
Last update
2026/08/27 01:53 (dev-main)
License
Downloads
0

Comments
comments powered by Disqus

[!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

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:

  1. Successful Laravel failed-job persistence is unchanged.
  2. Original bytes are available before diagnostic parsing.
  3. Quarantine storage failures cannot create a recursive failure loop.
  4. 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 --raw exports to public issues or general chat systems.
  • Monitor QuarantineCaptureFailed.
  • Never log $incident->rawPayload in 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.