Most queue debates I sit in are really arguments about operations, not throughput. A team picks SQS because it sounds serious, then discovers the 15-minute delay ceiling the week they build trial-expiry reminders. Another team picks Redis for speed, then loses jobs after an eviction policy nobody checked. A third team ships on the database driver and quietly runs fine for three years. The right broker is the one whose failure modes you can live with.

I wrote this with help from AI tools and checked every config, limit and code sample myself. I run Loop Origin, where we design and operate queue-heavy Laravel and Node systems for SaaS clients, and this is Part 6, the finale of Reliable Queues & Async Work in SaaS. Part 1 covered idempotent jobs. Part 2 covered backpressure and tenant isolation. Part 3 covered dead-letter queues and safe replay. Part 4 covered delayed jobs and clock skew. Part 5 covered correlation IDs and job traces. Here we choose the broker underneath all of it.

📚 Series Roadmap — Reliable Queues & Async Work in SaaS

Related: Multi-Tier Caching & Stampede Protection with Redis/Valkey

Should a Laravel or Node SaaS use Redis, SQS or a database queue?

Direct answer: Start with a database queue if you already run Postgres or MySQL 8 and process under a few hundred jobs per second; it is transactional and easy to inspect. Move to Redis (Horizon or BullMQ) for low latency and rich tooling. Choose SQS when you want managed durability and scale and can live with its delay and visibility limits.

Blurred teal status lights across a dark server rack, AI-generated

The comparison that actually matters

Concern Database (Postgres / MySQL 8) Redis / Valkey Amazon SQS
Durability Same as your DB (WAL, backups) Depends on AOF/RDB + eviction policy Managed, multi-AZ
Enqueue in same transaction as data Yes, natively No (needs outbox) No (needs outbox)
Typical latency 10–500 ms (poll interval) Low ms (blocking pop) Tens of ms (long poll)
Max delay Unlimited (available_at) Unlimited (sorted set) 15 minutes per message
Visibility / reservation reserved_at + retry_after retry_after / lock TTL Visibility timeout, max 12 h
Message size Row size (keep small) Memory bound 1 MiB (raised from 256 KiB in 2025)
Ordering ORDER BY id per queue Per list / per BullMQ queue Standard: none; FIFO: per message group
Tooling SQL Horizon, Bull Board CloudWatch, DLQ redrive
Ops burden None extra Run/patch Redis, memory alerts IAM, cost per request
Failure mode you will meet Lock contention, table bloat Silent loss on eviction or failover Delay ceiling, visibility timeouts too short

Read the last row twice. It is the row that decides most real choices.

Database queues: boring, transactional, underrated

Laravel's database driver uses SELECT … FOR UPDATE SKIP LOCKED on MySQL 8 and Postgres, so multiple workers do not fight over the same row. The killer feature is atomic enqueue: the job row commits with your business data or not at all, which removes a whole class of "order saved, email never queued" bugs.

// config/queue.php 'database' => [ 'driver' => 'database', 'connection' => 'pgsql', 'table' => 'jobs', 'queue' => 'default', 'retry_after' => 120, // > longest job runtime 'after_commit' => true, // dispatch only once the outer transaction commits ], 
-- Keep the hot path fast and the table small CREATE INDEX CONCURRENTLY jobs_queue_available_idx ON jobs (queue, available_at) WHERE reserved_at IS NULL; -- Nightly: prune processed failed_jobs and vacuum 

Where it hurts: very high throughput, long-running transactions holding locks, and table bloat on Postgres if autovacuum is not tuned. For many B2B SaaS products doing thousands of jobs per minute, not per second, it is plenty.

Redis: fast, flexible, and easy to lose data with

Redis gives you blocking pops, cheap delayed jobs via sorted sets, and great dashboards: Laravel Horizon on the PHP side, BullMQ with Bull Board on Node. The trap is that Redis is often configured as a cache.

# redis.conf for a queue instance (not your cache instance) maxmemory-policy noeviction # evicting jobs = silent data loss appendonly yes appendfsync everysec # at most ~1s of jobs lost on crash 
// Node + BullMQ worker with sane defaults import { Worker } from 'bullmq'; new Worker('invoices', processInvoice, { connection: { host: process.env.REDIS_HOST, maxRetriesPerRequest: null }, concurrency: 10, lockDuration: 60_000, // > p99 job time, or jobs get re-run limiter: { max: 50, duration: 1000 }, }); 

Run queues on a separate Redis from cache, alert on memory at 70%, and remember that a managed failover can still drop the last writes. Your Part 1 idempotency keys are what make that survivable.

SQS: managed durability with sharp edges

SQS removes servers from the equation and scales without thinking. The edges you will hit:

  • Delay ceiling: DelaySeconds maxes at 900. Laravel's SQS driver cannot delay a job longer than 15 minutes, so trial-expiry and "remind in 3 days" jobs need a due-at table plus sweeper, exactly as Part 4 described.
  • Visibility timeout: if a job runs longer than the timeout, another worker receives it. Set it above your p99, and extend it for long jobs.
  • FIFO throughput: FIFO queues order per MessageGroupId; use tenant ID as the group to get Part 2's fairness for free, and enable high-throughput mode if you need more than the default limits.
  • No transactional enqueue: use an outbox table and a relay.
// config/queue.php 'sqs' => [ 'driver' => 'sqs', 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'prefix' => env('SQS_PREFIX'), 'queue' => env('SQS_QUEUE', 'default'), 'region' => env('AWS_DEFAULT_REGION', 'eu-west-1'), 'after_commit' => true, ], 

Pair every SQS queue with a redrive policy (maxReceiveCount 5–10) into a DLQ, and you get Part 3's replay story with native tooling.

A decision path I use with clients

  1. Do you need enqueue atomic with business writes and run under ~200 jobs/sec? Database queue. Stop here.
  2. Do you need sub-10ms pickup, rich dashboards, or heavy rate limiting per tenant? Redis on a dedicated, noeviction instance.
  3. Are you on AWS, want zero broker ops, and can keep delays under 15 minutes (or use a due-at table)? SQS with outbox + DLQ.
  4. Mixed? Common and fine: database for transactional jobs, Redis or SQS for high-volume fan-out.

When we scope Laravel development work, broker choice is a one-hour conversation driven by these four questions, not a week of benchmarks. If you are weighing the cost of running it yourself against handing it off, our pricing page lays out the engagement options.

Before/after: a migration that should not have happened

Before (composite of real patterns): A team moved from the database driver to SQS "for scale" at about 20 jobs per second. Within a month: reminder jobs failing on the 15-minute limit, duplicate webhooks because report jobs outlived a 30-second visibility timeout, and orders committed without their follow-up job because enqueue was outside the transaction.

After: Transactional jobs moved back to Postgres with after_commit and a partial index. Only the high-volume webhook fan-out stayed on SQS, with tenant-keyed FIFO groups, a 10-minute visibility timeout and a DLQ. Fewer moving parts, fewer pages.

Diagnostic checklist

  1. Do you know your real peak jobs per second, not the hoped-for number?
  2. Must any job be enqueued atomically with a database write? If so, do you use the DB driver or an outbox?
  3. Do any jobs need delays longer than 15 minutes, and is that handled on SQS?
  4. Is retry_after / visibility timeout / lockDuration above your p99 job runtime?
  5. Is your queue Redis separate from cache and set to noeviction with AOF on?
  6. Is there a partial index on (queue, available_at) for database queues?
  7. Does every queue have a DLQ or failed-jobs table with a replay runbook?
  8. Is tenant fairness enforced (per-tenant queues, FIFO groups or rate limits)?
  9. Do dashboards show oldest-message age per queue (Part 5)?
  10. Have you tested a broker restart or failover and confirmed idempotency held?

FAQ

Is the Laravel database queue production-ready?

Yes, on MySQL 8+ or Postgres with SKIP LOCKED. Plenty of SaaS products run on it comfortably at moderate volume.

Redis or Valkey?

Functionally interchangeable for Horizon and BullMQ today. Pick based on your host's managed offering and licensing preference.

Can I use SQS with BullMQ?

Not directly; BullMQ is Redis-only. On Node with SQS, use the AWS SDK with a small consumer or a library such as sqs-consumer.

What about RabbitMQ or Kafka?

RabbitMQ fits complex routing; Kafka fits event streams and replayable logs. For typical SaaS background jobs they add more operations than value.

How do I migrate brokers safely?

Run both: new jobs go to the new broker, old workers drain the old one. Idempotency keys make overlap harmless.

Does the choice change for multi-region?

Yes. SQS is regional, Redis replication is asynchronous, and database queues follow your DB topology. Keep jobs region-local where you can.

Series wrap — Reliable Queues & Async Work in SaaS

Across six parts we built the queue stack from the effect outward: idempotent jobs so retries are harmless, backpressure and tenant fairness so one customer cannot starve the rest, DLQs and replay for poison messages, due-at schedulers that survive clock skew, correlation IDs that make async work debuggable, and finally a broker choice based on failure modes rather than hype. Get those right and Redis, SQS or a plain table all become reliable.

Follow & Connect