log in
consulting hosting industries the daily tools about contact

The Queue Failure You Don't See Coming Makes Redis Lonely

I've run Redis on my own boxes for years. I still do sometimes. But the failure mode comparison with managed queues changed how I think about the tradeoff.

I've had Redis save me and Redis betray me. After running it on my own boxes for the better part of a decade - for caching, sessions, queues, pub/sub - I have a lot of opinions about where it shines and where it quietly waits to ruin your weekend. The failure mode comparison with managed queues like SQS is what finally pushed me toward the boring choice for most new client work.

This isn't a hot take. It's a pattern I kept seeing until I couldn't unsee it.

The Problem Both Solutions Are Solving

You have work that shouldn't block an HTTP response. Send this email. Process this HL7 message from the EHR. Resize this image. Charge this card on retry. You push that work onto a queue, a worker picks it up, life is good.

The question isn't whether to use a queue. The question is what sits underneath it and what happens when that thing has a bad day.

With Laravel, the queue abstraction is genuinely good. You can swap drivers with one env var. Same dispatch() call, same job classes, same php artisan queue:work. The driver is almost invisible - until it isn't.

How Redis Fails (The Parts Nobody Blogs About)

Redis is fast, simple, and well-understood. I'm not here to bash it. But when I look at how Redis-backed queues have actually failed on me in production, the pattern is consistent.

Memory fills up and you get silent job loss. Redis has a maxmemory policy. If you're not watching it, and your eviction policy is set to something like allkeys-lru (common for caching), it will evict queue keys when memory pressure hits. Your jobs disappear. No error. No alert unless you built one. I've seen this on a shared Redis instance where someone kicked off a bulk export job that bloated the keyspace, and the queue jobs started evicting silently. Took us two hours to figure out why orders weren't processing.

The box itself goes down. A managed queue is a distributed service with redundancy baked in. Your Redis box is a box. If it's on the same host as your app (and I've set it up that way for small clients, I won't pretend I haven't), you have a single point of failure that takes down both your app and your queue simultaneously. Redis Sentinel and Redis Cluster exist but they're real operational work to set up and maintain correctly.

Persistence is opt-in and often misconfigured. AOF and RDB snapshotting are both fine but they're not on by default in a way that makes sense for queues, and appendonly yes with appendfsync always has real write performance implications. Most people running Redis for queues are using the defaults and crossing their fingers. If the process crashes hard or the box loses power, you lose whatever wasn't flushed. For a cache, fine. For job queues containing payment retries or patient intake records, not fine.

Horizon makes it better but adds another process to babysit. Laravel Horizon is genuinely great for visibility into Redis-backed queues. But now you're running Horizon, keeping it alive under Supervisor, monitoring its memory, and making sure it restarts cleanly after deploys. That's not a complaint exactly - it's just more surface area.

How SQS Fails (It's a Shorter List)

SQS is AWS infrastructure. It's been running since 2006. The failure modes are different.

The main one that bites people is the visibility timeout. When a worker pulls a message, SQS hides it from other workers for a configurable window (default 30 seconds). If your job takes longer than that and you haven't extended the visibility timeout, SQS assumes the worker died and makes the message visible again. Now two workers are processing the same job. For idempotent jobs this is fine. For jobs that send emails or make external API calls, this is a bad day.

The second one is the 256KB message size limit. If you're tempted to serialize an entire Eloquent model into your job payload, SQS will stop you. This is actually a good constraint - it pushes you toward passing IDs and loading from the database in the job, which is the right pattern anyway. But it's a gotcha if you're not expecting it.

That's basically it. The service itself doesn't go down in any way I've experienced or that meaningfully affects a typical application.

The Code Looks Almost Identical

This is the part I want to emphasize. Switching to SQS in Laravel is not a rewrite. Here's a job that could run on either:

<?php

namespace App\Jobs;

use App\Models\Order;
use App\Services\FulfillmentService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class FulfillOrder implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60;

    public function __construct(public readonly int $orderId)
    {
        // Pass the ID, not the model - SQS 256KB limit enforces good habits
    }

    public function handle(FulfillmentService $service): void
    {
        $order = Order::findOrFail($this->orderId);
        $service->fulfill($order);
    }

    public function failed(\Throwable $e): void
    {
        // Log it, notify, whatever - same as Redis
        logger()->error('FulfillOrder failed', [
            'order_id' => $this->orderId,
            'error' => $e->getMessage(),
        ]);
    }
}

Dispatching it:

FulfillOrder::dispatch($order->id);

The .env diff to switch drivers:

# Redis
QUEUE_CONNECTION=redis

# SQS
QUEUE_CONNECTION=sqs
AWS_DEFAULT_REGION=us-west-2
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
SQS_PREFIX=https://sqs.us-west-2.amazonaws.com/123456789/
SQS_QUEUE=your-queue-name

You'll need aws/aws-sdk-php via Composer, but that's it. The job class doesn't change. The dispatch call doesn't change. Your worker command doesn't change.

The Visibility Timeout Fix

The gotcha I mentioned about visibility timeouts is real but solvable. For long-running jobs, extend the timeout inside the job:

public function handle(FulfillmentService $service): void
{
    // Tell SQS we're still alive every 20 seconds
    // Default visibility timeout is 30s, so this keeps us safe
    $this->job->extend(30);

    $order = Order::findOrFail($this->orderId);
    $service->fulfill($order);
}

Or set a longer visibility timeout on the queue itself if all your jobs are long-running. SQS allows up to 12 hours.

When I Still Reach for Redis

I'm not saying throw Redis away. I run it for caching on almost everything. I still use Redis queues on projects where:

  • The client is already paying for a managed Redis service like Upstash or Redis Cloud, so the operational burden disappears
  • The team is already running Horizon and has muscle memory around it
  • The job payloads genuinely benefit from Redis-specific features like sorted sets for delayed jobs with fine-grained scheduling
  • It's an internal tool where a queue hiccup isn't catastrophic

And local development - Redis in Docker is fast and easy and I'm not changing that workflow.

When I Reach for SQS

For any client work where the queue is processing something that matters - payments, order fulfillment, healthcare data, notifications that go to real humans - I default to SQS now. The reasons:

  • AWS manages availability. I don't page at 2am because the queue service is down.
  • Dead-letter queues are first-class, built in, easy to configure. Failed jobs go somewhere I can inspect and replay.
  • The cost is basically zero at normal application scale. SQS is $0.40 per million requests. A busy app doing 10,000 jobs a day costs you $1.20 a month.
  • It scales without me doing anything. If a client's order volume 10x's over Black Friday, the queue handles it.

I integrated SQS for a specialty e-commerce client a couple years ago after they had a Redis eviction incident during a flash sale. We lost about 40 order fulfillment jobs silently over 90 minutes. Moving to SQS was a half-day project. They haven't had a queue incident since.

The Boring Choice Is Boring for Good Reasons

There's a version of this post where I talk about operational maturity and reliability engineering. I'm not going to write that version. The short version is: Redis on your own box asks you to solve a distributed systems problem on top of your application problem. SQS already solved it.

The code you write is the same either way. The thing that's different is who gets paged when the infrastructure has a bad day. I'd rather it be Amazon's team than mine.

Related

Need help shipping something like this? Get in touch.