Queuewatch: Building a Laravel Queue Monitoring Service
After years of working with Laravel queues, I've learned that the most dangerous failures are the ones you don't know about. Last month, I launched Queuewatch.io to solve this problem. Here's what I learned building a monitoring service for Laravel's queue system.
The Silent Failure Problem
Laravel's queue system handles failures elegantly. Jobs retry automatically, failed attempts get logged to the failed_jobs table, and the system continues processing. But there's a critical gap: nobody gets notified when things go wrong.
Consider this scenario: Your payment processing job starts failing at 2 AM because a third-party API is returning 500 errors. Laravel dutifully retries the job three times, logs the failure, and moves on. Hours later, customers start complaining about missing receipts. You check the failed_jobs table and discover dozens of payment confirmations that never went out.
This reactive workflow costs time and damages customer trust. The solution isn't better error handling it's better observability.
What Queuewatch Does
Queuewatch sits between your Laravel application and your notification channels, monitoring queue failures and alerting you immediately through Slack, Discord, email, or webhooks. But it goes beyond simple alerts:
Instant Failure Notifications with configurable rules. Filter by environment, job type, or failure frequency. Critical payment jobs trigger immediate Slack alerts, while low-priority notifications get batched into daily digests.
Rich Exception Context captured at failure time. Full stack traces, job payloads (optional), server context, and Laravel-specific metadata. Debug faster without SSH-ing into production servers.
Remote Retry directly from the dashboard. No need to run artisan queue:retry commands. Click "Retry" and the job gets re-dispatched to your application.
Enhanced CLI with powerful filtering:
# Filter by multiple criteria
php artisan queue:failed --queue=emails --after=yesterday
# Export as JSON for automation
php artisan queue:failed --json | jq '.[] | select(.exception | contains("Connection"))'
# Combine filters for precise queries
php artisan queue:failed --queue=payments --after="2025-12-01" --class=ProcessOrder
Implementation Deep Dive
Building Queuewatch required solving several interesting technical challenges. Here's how the system works under the hood.
The Laravel Package
Queuewatch installs as a Composer package that hooks into Laravel's queue failure events:
composer require queuewatch/laravel
The package registers a Queue::failing() listener during service provider boot:
namespace Queuewatch\Laravel;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Queue;
class QueuewatchServiceProvider extends ServiceProvider
{
public function boot()
{
Queue::failing(function (JobFailed $event) {
$this->app->make(QueuewatchClient::class)
->reportFailure($event);
});
}
}
When a job fails, Laravel fires the JobFailed event. The package captures comprehensive context:
public function reportFailure(JobFailed $event): void
{
$payload = [
'job_id' => $event->job->uuid() ?? $event->job->getJobId(),
'job_class' => $event->job->getName(),
'queue' => $event->job->getQueue(),
'connection' => $event->connectionName,
'exception' => [
'class' => get_class($event->exception),
'message' => $event->exception->getMessage(),
'trace' => $event->exception->getTraceAsString(),
'file' => $event->exception->getFile(),
'line' => $event->exception->getLine(),
],
'attempts' => $event->job->attempts(),
'max_tries' => $this->getMaxTries($event->job),
'environment' => config('queuewatch.environment'),
'project' => config('queuewatch.project'),
'php_version' => PHP_VERSION,
'laravel_version' => $this->app->version(),
'hostname' => gethostname(),
'timestamp' => now()->toIso8601String(),
];
if (config('queuewatch.capture_payload')) {
$payload['job_payload'] = $this->sanitizePayload(
$event->job->payload()
);
}
$this->client->post('/api/failures', $payload);
}
Handling Sensitive Data
One design challenge was balancing visibility with security. Job payloads might contain PII, API keys, or other sensitive data. Queuewatch solves this with multiple layers:
1. Payload Capture is Optional
// config/queuewatch.php
return [
'capture_payload' => env('QUEUEWATCH_CAPTURE_PAYLOAD', false),
];
By default, payloads aren't captured. You only see that ProcessOrder failed, not the order details.
2. Sanitization Pipeline
When payload capture is enabled, sensitive fields get redacted:
private function sanitizePayload(array $payload): array
{
$sensitive = config('queuewatch.sensitive_keys', [
'password', 'api_key', 'token', 'secret', 'ssn',
'credit_card', 'cvv', 'private_key'
]);
return $this->recursiveRedact($payload, $sensitive);
}
private function recursiveRedact(array $data, array $keys): array
{
foreach ($data as $key => &$value) {
if (is_array($value)) {
$value = $this->recursiveRedact($value, $keys);
} elseif ($this->isSensitiveKey($key, $keys)) {
$value = '[REDACTED]';
}
}
return $data;
}
3. Granular Ignore Rules
The configuration allows excluding specific jobs, queues, or exceptions:
// config/queuewatch.php
return [
'ignored_jobs' => [
App\Jobs\NoisyTestJob::class,
],
'ignored_queues' => [
'low-priority',
],
'ignored_exceptions' => [
Illuminate\Database\Eloquent\ModelNotFoundException::class,
],
];
This prevents alert fatigue from expected failures or noisy development environments.
The Remote Retry System
Implementing remote retry required careful security consideration. The flow works like this:
- User clicks "Retry" in the Queuewatch dashboard
- Dashboard sends authenticated request to Queuewatch API with the failed job ID
- API validates the request and dispatches a webhook to your Laravel application
- Your Laravel application receives the webhook, validates it, and retries the job
The webhook validation uses HMAC signatures:
public function verifyWebhook(Request $request): bool
{
$signature = $request->header('X-Queuewatch-Signature');
$payload = $request->getContent();
$expectedSignature = hash_hmac(
'sha256',
$payload,
config('queuewatch.webhook_secret')
);
return hash_equals($expectedSignature, $signature);
}
This ensures that only authenticated requests from Queuewatch can trigger retries on your application.
Scaling Considerations
During development, I hit several scaling challenges:
Challenge 1: API Rate Limiting
When a cascading failure causes hundreds of jobs to fail simultaneously, the package could overwhelm the Queuewatch API. Solution: Built-in batching and rate limiting:
private function sendBatch(array $failures): void
{
if (count($this->batch) >= $this->batchSize ||
$this->lastSentAt < now()->subSeconds(5)) {
$this->client->post('/api/failures/batch', [
'failures' => $this->batch
]);
$this->batch = [];
$this->lastSentAt = now();
}
}
Challenge 2: Network Failures
What happens when the monitoring service itself is unreachable? The package uses a fire-and-forget approach with timeout protection:
private function reportFailure(array $payload): void
{
try {
$this->client->timeout(2)->post('/api/failures', $payload);
} catch (Exception $e) {
// Never let monitoring failures break the application
if (config('queuewatch.debug')) {
logger('QueueWatch reporting failed: ' . $e->getMessage());
}
}
}
Monitoring failures never disrupt your application. Failed reports are logged (if debug mode is enabled) but don't throw exceptions.
The Dashboard Architecture
The Queuewatch dashboard is built with Laravel, Inertia.js, and React, the same stack I use for all my SaaS products. This allowed me to move quickly while maintaining type safety:
// TypeScript interfaces for type-safe API responses
interface QueueFailure {
id: string;
job_class: string;
queue: string;
connection: string;
exception: {
class: string;
message: string;
trace: string;
};
attempts: number;
max_tries: number;
environment: string;
failed_at: string;
acknowledged: boolean;
}
// React component with full type safety
export default function FailuresIndex() {
const { failures } = usePage<{ failures: QueueFailure[] }>().props;
const retry = (failureId: string) => {
router.post(`/failures/${failureId}/retry`);
};
return (
<div>
{failures.map(failure => (
<FailureCard
key={failure.id}
failure={failure}
onRetry={() => retry(failure.id)}
/>
))}
</div>
);
}
Multi-Application Support
Most teams run multiple Laravel applications: production, staging, and feature environments. Queuewatch handles this with a project/environment hierarchy:
- Projects represent distinct applications (your main app, admin panel, API)
- Environments represent deployment stages (production, staging, development)
Each installation gets tagged with project and environment identifiers:
QUEUEWATCH_PROJECT=my-app
QUEUEWATCH_ENVIRONMENT=production
The dashboard aggregates failures across all environments, with filtering by project, environment, or both. This prevents alert confusion when a staging deployment breaks temporarily.
Notification Configuration
The notification system uses a rule-based engine. You can create rules like:
- Alert on Slack when production payment jobs fail
- Send daily digest emails for staging failures
- Post to Discord when any job fails more than 5 times in an hour
- Trigger webhooks for specific exception types
Each rule matches against failure attributes:
class NotificationRule
{
public function matches(QueueFailure $failure): bool
{
return $this->matchesEnvironment($failure)
&& $this->matchesJobClass($failure)
&& $this->matchesQueue($failure)
&& $this->matchesFrequency($failure);
}
}
This flexibility prevents alert fatigue while ensuring critical failures get immediate attention.
What's Next
Queuewatch is live and monitoring queues for early customers. The roadmap includes:
- Performance metrics: Track average job duration, throughput, and queue depth trends
- Anomaly detection: ML-powered alerts for unusual failure patterns
- Advanced filtering: Save custom views and share them with your team
- Integrations: PagerDuty, Opsgenie, and other incident management platforms
- Open source package enhancements: More CLI commands and better local development tools
Try Queuewatch
Queuewatch is available now at queuewatch.io. The installation takes less than 5 minutes:
composer require queuewatch/laravel
Add your API key to .env, configure notifications in the dashboard, and catch queue failures before your customers do.
The package is open source (MIT licensed), and the service offers a free tier for small projects. For Laravel developers tired of discovering queue failures hours too late, Queuewatch provides the early warning system your queues have been missing.