# Hangfire Alternatives for Webhook Retries: Hookdeck Event Gateway, MassTransit, Azure Service Bus, and Convoy Compared

Hangfire is so embedded in .NET that it becomes 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, call `BackgroundJob.Enqueue()`, return a 200. It's free, it ships with a dashboard, and it stores jobs in the SQL Server you already run.

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: SQL Server becomes a queue (a job it was never great at), signature verification code multiplies across providers, a refactored method name breaks every queued and failed job that referenced it, and when retries finally exhaust, the Failed jobs page shows you a serialized method call 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](/event-gateway), message buses like MassTransit and NServiceBus, cloud queues like Azure Service Bus and Amazon SQS, and Convoy. None of these replace Hangfire 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 [Laravel in PHP](/webhooks/platforms/laravel-queue-alternatives-for-webhook-retries), [Sidekiq in Rails](/webhooks/platforms/sidekiq-alternatives-for-webhook-retries), [Celery in Python](/webhooks/platforms/celery-alternatives-for-webhook-retries), and [BullMQ in Node](/webhooks/platforms/bullmq-alternatives-for-webhook-retries).

## How to evaluate a Hangfire alternative for webhook retries

Webhook retries look like a background-job problem, but the evaluation criteria differ from general 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 C#? 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 job library 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? Hangfire's Failed state stores a serialized method invocation and an exception. The webhook that caused it is whatever survived argument serialization.
* 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 and monitor? SQL Server under queue-shaped load, Hangfire server processes, dashboard hosting, cleanup jobs. Each is a service you operate and a way to be paged.
* .NET ecosystem fit. Does the alternative require rewriting controllers and jobs, or does it sit upstream and hand your existing endpoint pre-verified events?
* Cost. Not just licenses (Redis storage and batches require Hangfire Pro, from $500/year): database load, worker compute, and the engineering hours that go into maintaining webhook plumbing instead of product.

Keep these in mind as we go through what Hangfire actually gives you.

## What Hangfire does (and why .NET developers reach for it on webhooks)

Hangfire is the closest thing .NET has to a default background-job system: fire-and-forget, delayed, and recurring jobs as plain method calls, persisted to storage, executed by worker threads, with a built-in dashboard. The core library is free under LGPL 3.0, runs inside your ASP.NET Core process via `AddHangfireServer()` or in a separate worker service, and uses SQL Server as its standard storage so most teams adopt it without any new infrastructure.

### Hangfire key features

* Automatic retries. The global `AutomaticRetryAttribute` retries failed jobs 10 times by default with increasing delays, configurable per method (`[AutomaticRetry(Attempts = 5)]`) or globally through job filters.
* Persistent, transactional-ish job storage. Jobs survive restarts because they're rows in SQL Server (or Redis with Hangfire Pro). Enqueueing is a database write you can reason about.
* The dashboard. Succeeded, Processing, Failed, and Scheduled jobs with exception details and manual retry, hosted at `/hangfire` in your own app.
* Recurring jobs. `RecurringJob.AddOrUpdate()` with cron expressions covers the scheduler use case that .NET teams would otherwise solve with Quartz.NET.
* Method-call ergonomics. `BackgroundJob.Enqueue(() => ProcessEvent(json))` feels like calling a function. No message contracts, no broker topology.

### Why Hangfire gets 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 .NET resource says the same thing: get the work off the request path. So you write this:

```csharp
[ApiController]
[Route("webhooks/stripe")]
public class StripeWebhookController : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> Handle()
    {
        var json = await new StreamReader(Request.Body).ReadToEndAsync();

        var stripeEvent = EventUtility.ConstructEvent(
            json,
            Request.Headers["Stripe-Signature"],
            _webhookSecret); // throws on bad signature

        BackgroundJob.Enqueue<StripeEventProcessor>(p => p.Process(json));

        return Ok();
    }
}

```

And a processor with a retry policy:

```csharp
public class StripeEventProcessor
{
    [AutomaticRetry(Attempts = 10)]
    public void Process(string json)
    {
        // process the event
    }
}

```

This is a real improvement over processing inline. The provider gets a fast 200, transient failures retry automatically, and the dashboard shows the queue is healthy. For one provider at modest volume, it's fine.

## Where Hangfire starts to hurt in a webhook context

The problems arrive with scale, with provider count, or with the first incident that makes you realize what you can't see.

SQL Server becomes a queue. Hangfire's standard storage polls SQL Server for work, and every webhook becomes an insert, several state-transition updates, and eventually a delete, all against the same database serving your application. At volume this means lock contention, index churn, and a `Job` table that grows faster than anything else you own. On Azure SQL, webhook bursts translate directly into DTU spikes. The fix Hangfire offers is Redis storage, which is faster but requires a Hangfire Pro subscription and adds the Redis operations that teams on other stacks are trying to escape.

Serialized method calls are fragile. `BackgroundJob.Enqueue(() => ProcessEvent(json))` persists the type name, method name, and serialized arguments. Rename the method, move the class, or change a parameter type, and every queued and failed job referencing it starts throwing deserialization errors after your next deploy. Failed webhooks waiting for a fix can be broken by the fix. This is a known Hangfire sharp edge, and webhook retries (jobs that deliberately live for hours or days) sit exactly where it cuts.

Signature verification multiplies. Stripe.net's `EventUtility.ConstructEvent` handles Stripe. Then you add Shopify, and you're computing an HMAC over the raw body, which in ASP.NET Core means `EnableBuffering()` and careful stream handling before model binding gets there first. Then GitHub, with a different header format. 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 you now maintain. See [how to implement SHA256 webhook signature verification](/webhooks/guides/how-to-implement-sha256-webhook-signature-verification) for what this involves per provider.

Spikes still hit your app first. Kestrel handles concurrent requests well, but every webhook still performs a synchronous SQL write on the request path. A flash sale's worth of Shopify webhooks becomes a write storm against the same database your users are reading from. The queue never got the chance to absorb the spike, because the bottleneck is the enqueue itself. Ingestion capacity is whatever your app and database can absorb in the moment, and you can't scale it independently of them.

The Failed page is job-centric, and Succeeded jobs evaporate. After the last retry, the dashboard shows an exception, a stack trace, and the serialized arguments. What you want to know is: which Stripe event was this, what were its headers, and what happened on each attempt? Worse, succeeded jobs expire after one day by default, so when a customer asks "did you process order 4521's webhook last Tuesday?", the answer is gone. Reconstructing history means correlating application logs with the provider's dashboard.

Retry policy is compiled in. `[AutomaticRetry(Attempts = 10)]` is an attribute on a method. Giving Stripe a three-day window while capping a chatty analytics provider at an hour means separate processor classes or filter gymnastics, 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.

None of this makes Hangfire a bad tool. Webhook ingestion simply has requirements (verification, durable acceptance, per-source policy, event-level visibility) that a general-purpose job library was never designed to own.

## Hangfire alternatives for webhook retries

### Hookdeck Event Gateway

[Hookdeck Event Gateway](/event-gateway) is managed webhook infrastructure that sits upstream of your .NET 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:

```mermaid
flowchart LR
    A[Stripe] --> B[ASP.NET controller<br/> signature verification + retry policy in code]
    B --> C[SQL Server job storage]
    C --> D[Hangfire server]
    D --> E[Handler logic]

```

After:

```mermaid
flowchart LR
    A[Stripe] --> B[Hookdeck<br/> signature verification + retry policy as config]
    B --> C[ASP.NET 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](/docs/sources) 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; your database never sees the write 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](/docs/retries).
* Issues and event-level observability. Every event with full headers, body, and per-attempt delivery history, plus full-text search across payloads. History doesn't expire after a day, and failed deliveries become [Issues](/docs/issues) with alerting.
* One-click replay. Replay any event, individually or in bulk, as the original HTTP request. A renamed C# method can't break a stored HTTP event.
* Local development with the CLI. `hookdeck listen 5000` forwards real webhooks to your local Kestrel server, with no ngrok tunnel, event history that survives restarts, and shared access for a whole team.

#### How does Hookdeck Event Gateway compare to Hangfire?

<table>
<tr>
<th><strong>Capability</strong></th>
<th><strong>Hookdeck Event Gateway</strong></th>
<th><strong>Hangfire</strong></th>
</tr>
<tr>
<td>Webhook awareness</td>
<td>✅ 160+ sources with built-in signature verification</td>
<td>❌ Hand-rolled per provider (Stripe.net covers Stripe only)</td>
</tr>
<tr>
<td>Retry policy</td>
<td>✅ Per-connection, changed in the dashboard</td>
<td>❌ <i>[AutomaticRetry]</i> attributes, changed by deploy</td>
</tr>
<tr>
<td>Spike protection</td>
<td>✅ Absorbed at the gateway, rate-limited delivery</td>
<td>❌ Every webhook is a synchronous SQL write on the request path</td>
</tr>
<tr>
<td>Failed-event visibility</td>
<td>✅ Issues with full payload, headers, attempt history, search</td>
<td>ℹ️ Failed page: serialized method call + exception; succeeded jobs expire in a day</td>
</tr>
<tr>
<td>Replay</td>
<td>✅ One-click replay of the original event</td>
<td>ℹ️ Manual retry re-invokes the serialized method (breaks after refactors)</td>
</tr>
<tr>
<td>Operations</td>
<td>✅ Managed</td>
<td>❌ SQL Server or Redis (Pro), Hangfire servers, cleanup: yours to run</td>
</tr>
<tr>
<td>Local development</td>
<td>✅ Via <i>hookdeck listen</i>, no tunnel, persistent history</td>
<td>ℹ️ ngrok plus local storage and a Hangfire server</td>
</tr>
<tr>
<td>Self-hosting</td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td>Non-webhook jobs</td>
<td>❌</td>
<td>✅ Recurring jobs, batches, everything</td>
</tr>
</table>
<br />

Pricing starts free, with paid plans from $39/month; see [hookdeck.com/pricing](/pricing). All paid tiers carry a 99.999% uptime SLA.

### MassTransit and NServiceBus

The .NET answer to "we've outgrown Hangfire" is often a message bus. MassTransit and NServiceBus give you real messaging semantics over RabbitMQ or Azure Service Bus: retries with backoff, error queues, sagas, and outbox patterns. If your system is heading toward event-driven architecture anyway, they're serious tools.

For webhook retries specifically, though, a bus moves the problem rather than solving it. You still write signature verification, your controllers still do the publishing (so ingestion still scales with your app), and a dead-lettered message shows you a message envelope stripped of the webhook context you'll want during an incident. You've also taken on broker topology, consumer deployment, and versioned message contracts. Licensing deserves a look too: NServiceBus is commercial, and MassTransit's v9 is moving to a commercial license, so the "free bus" assumption is worth re-checking before you build on it. For the broader distinction, see [event gateway vs message broker](/webhooks/guides/event-gateway-vs-message-broker).

### Azure Service Bus, Amazon SQS, and cloud queues

Pointing your webhook controller at Azure Service Bus or SQS (often with an Azure Function or Lambda consuming) removes broker operations entirely: the cloud runs the queue and it scales without you. Dead-letter queues catch exhausted retries.

But a cloud queue is still a generic queue. You keep writing verification code, the enqueue still happens in your request path, dead-letter messages carry even less context than Hangfire's Failed page, and debugging now spans your app, the Azure portal or AWS console, and Application Insights or CloudWatch. You've traded database contention for cloud plumbing, and the webhook-specific gaps are untouched. See [AWS SQS alternatives for webhooks](/webhooks/platforms/aws-sqs-alternatives-for-webhooks) for a closer look.

### Convoy

[Convoy](https://getconvoy.io/) 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, and it's a Go service in a .NET shop. Its source catalog and observability depth are also thinner than Hookdeck's. You'd be swapping "operate a job store" for "operate a gateway": a better tool, but you're still going to get paged. For more, see the [Hookdeck Event Gateway vs Convoy comparison](/webhooks/platforms/hookdeck-event-gateway-vs-convoy-webhook-receiving-comparison).

## When to keep Hangfire

Keep it for almost everything. Recurring jobs, report generation, email dispatch, batch processing: Hangfire is well-built general-purpose infrastructure with a decade of production mileage, and nothing here argues otherwise.

Keeping it for webhook retries specifically is reasonable when volume is low, providers number one or two, and the failure modes above are theoretical for you. An internal app receiving a few hundred Stripe events a day does not need dedicated webhook infrastructure; Stripe.net and the default retry attribute will do fine. The calculus changes when webhooks become critical: 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 page at 2 a.m.

The most common end state isn't a migration away from Hangfire at all. It's a split: Hookdeck owns the webhook boundary, and Hangfire keeps doing everything else. Often the processor classes survive; they just start receiving pre-verified events.

## Migrating .NET webhook retries to Hookdeck Event Gateway

The migration is incremental and runs in parallel with your existing path:

1. Create a [source](/docs/sources) in Hookdeck for your provider (say, Stripe). You get a webhook URL with Stripe's signature verification already configured.
2. Create a connection from that source to your ASP.NET endpoint, say `https://app.example.com/webhooks/stripe`, and set the retry policy per source in the dashboard.
3. 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.
4. Simplify the controller. Verification has moved upstream, so the endpoint now receives a verified event. You can process inline or keep enqueueing a Hangfire job. Either way, retries for delivery failures now happen at the gateway, with full event context.
5. Keep Hangfire for everything else. Nothing about recurring jobs, batches, or reports changes.

For local development, run `hookdeck listen 5000` 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](/docs/cli)

For the broader pattern, see [managed webhook gateway vs DIY queue-backed infrastructure](/webhooks/guides/managed-webhook-gateway-vs-diy-queue-backed-infrastructure).

## Hookdeck Event Gateway is the managed answer for webhook retries

Hangfire earned its place as .NET's default answer to background work, and it should keep that job. But webhook ingestion asks questions a general-purpose job library 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](/event-gateway) answers those upstream of your application. Verification, durable queueing, per-source retries, event-level observability, and replay come managed, with your .NET app receiving clean, verified events. Your controllers get simpler. Hangfire stays for what it's actually good at.