Laravel Queue Alternatives for Webhook Retries: Hookdeck Event Gateway, Database Driver, Amazon SQS, and Convoy Compared
Queues are so embedded in Laravel that they become the default tool for "webhook retries" the moment a controller starts seeing 5xx responses or a Stripe rate limit. The pattern is canonical: verify the signature, dispatch() a job, return a 200. It's in the Laravel docs, it's what Cashier does with Stripe events, and packages like Spatie's laravel-webhook-client have turned it into a convention.
That pattern works right up until webhooks stop being a side effect and start being critical. Then the problems show up in a predictable order: Redis (or your jobs table) becomes critical-path infrastructure, signature verification code multiplies across providers, a webhook storm from Shopify saturates PHP-FPM before a single job is queued, and when retries finally exhaust, the failed_jobs table shows you a serialized job and a stack trace instead of the event you actually lost.
This article looks at the alternatives for the webhook-specific part of that workload: Hookdeck Event Gateway, Laravel's own database driver, cloud queues like Amazon SQS, and Convoy. None of these replace Laravel queues wholesale, and they shouldn't. The question is which concerns belong upstream of your application, and what handles them best. Working in a different stack? See the equivalent guides for Sidekiq in Rails, Celery in Python, and BullMQ in Node.
How to evaluate a Laravel queue alternative for webhook retries
Webhook retries look like a queueing problem, but the evaluation criteria differ from general background jobs. Six dimensions matter:
- Webhook-aware semantics. Does the tool understand signatures, source identity, and event deduplication natively, or do you keep writing that logic in PHP? Stripe uses HMAC-SHA256 with a timestamp tolerance, Shopify uses HMAC-SHA256 over the raw body, GitHub uses HMAC-SHA256 with its own header format. A generic queue treats all of this as your problem.
- Failed-payload visibility. When a retry exhausts, can you see the original request (headers, body, timestamps, delivery attempts) or only a job-centric record? Laravel's
failed_jobstable stores a serialized job class and an exception. The webhook that caused it is buried inside the payload column, if it was captured at all. - Per-source retry policy. Stripe retries for up to three days on its side; GitHub doesn't automatically retry at all; Shopify cancels your webhook subscription after enough consecutive failures. A sensible retry policy differs per provider. Can you change it without a deploy?
- Operational footprint. What do you now run? Redis, Horizon, Supervisor-managed
queue:workprocesses, autoscaling rules, alerting. Each is a service you operate and a way to be paged. - Laravel ecosystem fit. Does the alternative require rewriting controllers and jobs, or does it sit upstream and hand your existing route pre-verified events?
- Cost. Not just the subscription: Redis instances, worker compute, and the engineering hours that go into maintaining webhook plumbing instead of product.
Keep these in mind as we go through what Laravel queues actually give you.
What Laravel queues do (and why they're so popular for webhooks)
Laravel's queue system is a first-class framework feature: a unified API over multiple backends (database, Redis, Amazon SQS, Beanstalkd), with jobs as plain PHP classes implementing ShouldQueue. Since Laravel 11 the default driver is database, which means every fresh Laravel app has a working queue before Redis enters the picture. Horizon adds a dashboard, worker supervision, auto-balancing, and metrics for Redis-backed queues.
Laravel queue key features
- Declarative retries.
public $tries = 5;andpublic $backoff = [60, 300, 900];on a job class, orretryUntil()for time-based limits. Failed jobs land in thefailed_jobstable and can be re-run withphp artisan queue:retry. - Multiple drivers, one API. Swap
QUEUE_CONNECTIONbetween database, Redis, SQS, and Beanstalkd without touching job code. - Batching, chaining, rate limiting.
Bus::batch(), job chains,WithoutOverlapping, and throttling middleware cover most orchestration needs. - Horizon. Real-time throughput, runtime, and failure metrics for Redis queues, with retry-from-dashboard and tag-based monitoring.
- Deep framework integration. Queued listeners, queued mail, queued notifications. The queue is the framework's answer to "do this later" everywhere.
Why Laravel queues get used for webhook retries
Because the path of least resistance leads straight there. A webhook controller doing slow work inline starts timing out or dropping events, and every Laravel resource (the docs, Laracasts, package READMEs) says the same thing: get the work out of the request. So you write this:
class StripeWebhookController extends Controller
{
public function __invoke(Request $request)
{
$signature = $request->header('Stripe-Signature');
if (! $this->verifySignature($request->getContent(), $signature)) {
abort(400);
}
ProcessStripeWebhook::dispatch($request->all());
return response()->json(['received' => true]);
}
}
And a job with a retry policy:
class ProcessStripeWebhook implements ShouldQueue
{
use Queueable;
public $tries = 5;
public $backoff = [60, 300, 900, 3600];
public function handle(): void
{
// process the event
}
public function failed(Throwable $exception): void
{
// notify someone, eventually
}
}
This is a real improvement over processing inline. The provider gets a fast 200, transient failures retry automatically, and Horizon shows you the queue is healthy. For one provider at modest volume, it's fine.
Where Laravel queues start to hurt for webhooks
The problems arrive with scale, with provider count, or with the first incident that makes you realize what you can't see.
The ack-before-dispatch gap. Your controller returns 200 after dispatch() succeeds. But if Redis is unavailable, or the process dies between verification and dispatch, the webhook is gone and the provider believes it was delivered. With the database driver inside a transaction, a rollback can silently discard the dispatch too (this is why afterCommit() exists, and why forgetting it is a classic bug). The provider won't retry an event you acknowledged.
Signature verification multiplies. Cashier handles Stripe's signature for you. Then you add Shopify, and you're doing hash_equals() against an HMAC of the raw body, being careful to use $request->getContent() and not the parsed input. Then GitHub, with a different header. Then Paddle, Twilio, WooCommerce. Each one is security-sensitive code with subtle failure modes (raw-body access, timing-safe comparison, timestamp tolerance), duplicated across controllers or half-abstracted into middleware. Spatie's laravel-webhook-client centralizes the pattern, but it's still your code running on your infrastructure, and it still stores payloads in your database.
Webhook spikes hit PHP-FPM first. The queue only protects you after the request is accepted. A flash sale sends thousands of Shopify webhooks in minutes, each one occupying a PHP-FPM worker for the duration of verify-and-dispatch. Your worker pool saturates, the same pool serving actual users, and the provider starts seeing timeouts. The queue never got the chance to absorb the spike, because the bottleneck is upstream of it. On Vapor the equation changes to Lambda concurrency and per-invocation cost, but the shape is the same: ingestion scales with your application, not independently of it.
The failed_jobs table is job-centric, not event-centric. After the last retry, you get a row: connection, queue, a serialized job payload, an exception trace, a timestamp. What you want to know is: which Stripe event was this, what were its headers, what did the provider send, and what happened on each attempt? Reconstructing that means deserializing payloads and cross-referencing the provider's dashboard. php artisan queue:retry re-runs the job class. It can't replay the original HTTP event, and if the job's data was truncated or the bug was in deserialization, the retry inherits the problem.
Retry policy lives in code. $tries and $backoff are properties on a job class. Giving Stripe a three-day window while capping a chatty analytics provider at an hour means either separate job classes per provider or conditional logic, and any change is a deploy. During an incident ("their API is down, extend retries and pause delivery"), a deploy is exactly what you don't want on the critical path.
Horizon can't see webhooks. Horizon tells you throughput, wait times, and failure counts: queue health. It doesn't know what a webhook is. There's no per-source view, no full-text search across event payloads, no answer to "did we receive Shopify order 4521's webhook and what did we do with it?" And Horizon itself requires Redis and processes you supervise, so the monitoring layer adds to the operational footprint it's monitoring.
None of this makes Laravel queues a bad tool. Webhook ingestion simply has requirements (verification, durable acceptance, per-source policy, event-level visibility) that a general-purpose job queue was never designed to own.
Laravel queue alternatives for webhook retries
Hookdeck Event Gateway
Hookdeck Event Gateway is managed webhook infrastructure that sits upstream of your Laravel application. Providers send webhooks to a Hookdeck URL; Hookdeck verifies signatures, queues events durably, delivers them to your endpoint with configurable retries, and gives you event-level observability across the whole flow. Your controller receives pre-verified events and just processes them.
The architectural shift looks like this:
Before:
flowchart LR
A[Stripe] --> B[Laravel controller signature verification + retry policy in code]
B --> C[Redis / jobs table]
C --> D[queue:work worker]
D --> E[Handler logic]
After:
flowchart LR
A[Stripe] --> B[Hookdeck signature verification + retry policy as config]
B --> C[Laravel controller]
C --> D[Handler logic]
Hookdeck Event Gateway key features
- 160+ pre-configured sources. Stripe, Shopify, GitHub, Twilio, Paddle, and a hundred more, with signature verification built in. Adding a provider is configuration, not a new controller and another HMAC implementation.
- Durable ingestion, independent of your app. Hookdeck acknowledges the provider and queues the event before your application is involved. A spike is absorbed at the gateway with rate-limited delivery to your endpoint; PHP-FPM never sees the storm. If your app is down or deploying, events wait and deliver when it's back.
- Per-connection retry policies, no deploy. Set Stripe to retry for days and a noisy source to give up in an hour, from the dashboard, live. Pause and resume delivery per source during incidents. See the retries documentation.
- Issues and event-level observability. Every event with full headers, body, and per-attempt delivery history, plus full-text search across payloads. Failed deliveries become Issues with alerting, and what you see is the event itself rather than a stack trace.
- One-click replay. Replay any event, individually or in bulk, as the original HTTP request rather than a re-run of a serialized job class.
- Local development with the CLI.
hookdeck listen 8000forwards real webhooks to your localartisan serve, with no ngrok or Expose tunnel, event history that survives restarts, and shared access for a whole team.
How does Hookdeck Event Gateway compare to Laravel queues?
| Capability | Hookdeck Event Gateway | Laravel queues + Horizon |
|---|---|---|
| Webhook awareness | ✅ 160+ sources with built-in signature verification | ❌ Hand-rolled per provider (Cashier covers Stripe only) |
| Retry policy | ✅ Per-connection, changed in the dashboard | ❌ $tries/$backoff in code, changed by deploy |
| Spike protection | ✅ Absorbed at the gateway, rate-limited delivery | ❌ Spikes hit PHP-FPM before the queue can help |
| Failed-event visibility | ✅ Issues with full payload, headers, attempt history, search | ℹ️ failed_jobs row: serialized job + exception |
| Replay | ✅ One-click replay of the original event | ℹ️ queue:retry re-runs the job class |
| Operations | ✅ Managed | ❌ Redis/DB, queue:work, Supervisor, Horizon: yours to run |
| Local development | ✅ Via hookdeck listen, no tunnel, persistent history | ℹ️ ngrok/Expose plus a local queue and worker |
| Self-hosting | ❌ | ✅ |
| Non-webhook jobs | ❌ | ✅ Mail, notifications, batches, everything |
Pricing starts free, with paid plans from $39/month; see hookdeck.com/pricing. All paid tiers carry a 99.999% uptime SLA.
Try Hookdeck Event Gateway free
Webhook ingestion, durable queueing, and per-source retries without operating Redis or Horizon
The database driver (and Laravel Cloud)
If your pain is specifically Redis (one more service to run, one more thing on the critical path), Laravel's database driver removes it. It's the framework default since Laravel 11, failed jobs work identically, and for low-to-moderate volume it's entirely serviceable. Laravel Cloud takes it further by managing the workers themselves, with autoscaling.
What it doesn't change: everything else on the list. Verification is still your code, spikes still hit your web tier first, failed_jobs is still job-centric, and under sustained webhook volume the jobs table itself becomes a contention point that Redis users moved away from for a reason. It simplifies the same architecture rather than changing it.
Amazon SQS and cloud queues
Laravel ships an SQS driver, so pointing QUEUE_CONNECTION=sqs at a managed queue removes broker operations entirely: AWS runs the queue and it scales without you. Google Cloud Tasks fills a similar role with an HTTP push model.
But a cloud queue is still a generic queue. You keep writing verification code, your controllers still do the enqueueing (so ingestion still scales with your app), dead-letter queues give you even less payload context than failed_jobs, and now debugging spans the Laravel app, the AWS console, and CloudWatch. You've traded Redis operations for AWS plumbing, and the webhook-specific gaps are untouched. See AWS SQS alternatives for webhooks for a closer look.
Convoy
Convoy is an open-source webhook gateway, a closer conceptual match than any general queue, with signature verification, retries, and a delivery dashboard. If self-hosting is a hard requirement (data residency, compliance, policy), it's the credible option in this list.
The trade-off is that self-hosting a webhook gateway means operating a webhook gateway: Postgres, Redis, and the service itself, sized for your spikes, monitored by you. Its source catalog and observability depth are also thinner than Hookdeck's. You'd be swapping "operate a queue" for "operate a gateway": a better tool for this use-case, but you're still going to get paged. For more, see the Hookdeck Event Gateway vs Convoy comparison.
When to keep Laravel queues
Keep them for almost everything. Mail, notifications, exports, scheduled work, batch processing: Laravel queues with Horizon are excellent general-purpose infrastructure, and nothing here argues otherwise.
Keeping them for webhook retries specifically is reasonable when volume is low, providers number one or two, and the failure modes above are theoretical for you. A side project receiving a few hundred Stripe events a day does not need dedicated webhook infrastructure; Cashier and the database driver will do fine. The calculus changes when webhooks become critical for you: multiple providers, revenue depending on events being processed, spikes correlated with your busiest traffic, or an on-call rotation that has already met the failed_jobs table at 2 a.m.
The most common end state isn't a migration away from Laravel queues at all. It's a split: Hookdeck owns the webhook boundary, and your queues keep doing everything else. Often the job classes survive; they just start receiving pre-verified events.
Migrating Laravel webhook retries to Hookdeck Event Gateway
The migration is incremental and runs in parallel with your existing path:
- Create a source in Hookdeck for your provider (say, Stripe). You get a webhook URL with Stripe's signature verification already configured.
- Create a connection from that source to your Laravel endpoint, say
https://app.example.com/webhooks/stripe, and set the retry policy per source in the dashboard. - Point the provider at Hookdeck. Update the webhook URL in Stripe's dashboard. Run both paths in parallel while you validate; Hookdeck's event log shows exactly what's flowing.
- Simplify the controller. Verification has moved upstream, so the route now receives a verified event. You can process inline or keep dispatching a job. Either way, retries for delivery failures now happen at the gateway, with full event context.
- Keep Laravel queues for everything else. Nothing about mail, notifications, or batches changes.
For local development, run hookdeck listen 8000 and real provider webhooks arrive at your local server: the same source configuration from development through CI to production, no tunnel, and the event history is there when you restart. Install the CLI.
For the broader pattern, see managed webhook gateway vs DIY queue-backed infrastructure.
Hookdeck Event Gateway is the managed answer for webhook retries
Laravel queues earned their place as the framework's default answer to background work, and they should keep that job. But webhook ingestion asks questions a general-purpose queue can't answer: who verifies the signature, what absorbs the spike, where does the failed event live, and how fast can you change a retry policy when a provider has a bad day.
Hookdeck Event Gateway answers those upstream of your application. Verification, durable queueing, per-source retries, event-level observability, and replay come managed, with your Laravel app receiving clean, verified events. Your controllers get simpler. Your queues stay for what they're actually good at.
Try Hookdeck Event Gateway for free
Webhook ingestion, durable queueing, retries, and observability, without operating Redis or Horizon
FAQs
What is the best alternative to Laravel queues for webhook retries?
It depends on the constraint. For managed, webhook-specific infrastructure with per-source retries and event-level observability, Hookdeck Event Gateway. If you only want to drop Redis and stay entirely in Laravel, the database driver (or Laravel Cloud's managed workers). If self-hosting is mandatory, Convoy.
Can I keep Laravel queues for background jobs and use something else just for webhook retries?
Yes, and that's the most common architecture. Hookdeck owns webhook ingestion, verification, and delivery retries; Laravel queues keep handling mail, notifications, and batch work. Your existing job classes often survive unchanged, minus the webhook-specific plumbing.
Aren't $tries and $backoff already good enough for webhooks?
The retry mechanics are solid. The gaps are everything around them: signature verification per provider, the ack-before-dispatch loss window, spikes saturating PHP-FPM before the queue helps, per-provider retry windows requiring deploys, and failed_jobs showing you a stack trace instead of the event. Retries were never the hard part.
Do I still need Horizon if I move webhook retries to Hookdeck?
If you run Redis queues for non-webhook work, yes: Horizon remains the right way to monitor them. What changes is scope. Webhook events get purpose-built observability (per-attempt history, payload search, Issues) at the gateway, and Horizon goes back to monitoring queue health, which is what it's for.
Does Hookdeck Event Gateway replace Redis or my queue driver?
Only for webhook traffic. Hookdeck provides the queueing and retries between the provider and your endpoint, so webhooks no longer depend on your Redis or jobs table. Most teams keep their existing queue driver for everything else.