Your Staging Environment Does Not Have Real Traffic. That Is the Problem.
The rewrite is done. You are confident. You ran it against every test case you could think of. You replayed 50 recorded events from production against the new handler in staging and they all passed.
You deploy to production. Within 6 hours, a customer reports that their subscription upgrade is not reflected in their account. You look at the logs. The new handler processed the event and returned 200. But the business logic for a specific edge case — a user who had a promotional discount applied during a previous billing cycle, then paused their subscription, then re-subscribed with the same payment method — produced incorrect state.
That edge case never appeared in your 50 recorded events. It appears in production roughly once every 400 subscription events. With 30,000 subscribers, you see it perhaps twice a week. You would have caught it in staging if staging had seen production traffic for a week before the switch.
This is not a hypothetical. This is the standard failure mode for handler migrations. The edge cases that matter most are the ones with the lowest frequency — which makes them essentially invisible in any test dataset that is not drawn from a large volume of real traffic.
Shadow delivery exists to solve this problem. Point your shadow target at your new handler. It receives every inbound event concurrently with your primary handler, silently, for as long as you want. When you have seen enough traffic — and enough of the rare edge cases — you switch. No customer is ever affected by shadow failures. No primary delivery is ever delayed.
How It Works
When shadow delivery is configured on a hook, every inbound event triggers two concurrent deliveries:
-
Primary delivery: your existing target URL. This is the delivery that matters. Its response code is what gets logged as the official outcome. Its latency is what your SLA is measured against. If it fails, circuit breaker logic applies.
-
Shadow delivery: your configured shadow target URL. This runs in parallel, after the primary delivery is dispatched. The shadow request is sent with the same payload, the same headers (with an additional
X-HookTunnel-Shadow: trueheader so your handler can identify it), and the same timeout budget.
The shadow response — status code, body, latency — is logged in the event detail. It never affects the primary outcome. If the shadow target returns 500, that is recorded in the event detail as a shadow failure. The provider sees nothing. The circuit breaker for the primary target is unaffected. Your system continues operating normally.
The primary delivery completes and the provider gets its acknowledgement. Shadow delivery happens on a parallel path. Primary delivery latency is not impacted.
The Migration Scenario
Your payment team is migrating from a monolithic payment webhook handler to a domain-specific handler that will only process subscription lifecycle events. The new handler is written. The tests are green. You need to validate it against production traffic before switching.
Here is what the migration looks like with shadow delivery:
Day 1: Configure the new handler's staging URL as the shadow target on the Stripe subscription hooks. Deploy the new handler to staging. Normal operation continues. Every subscription event now hits both the production handler (primary) and the staging handler (shadow).
Day 1 to Day 7: Seven days of production traffic flows silently through the staging handler. You watch the shadow failure rate in the event detail modal. For the first three days, the failure rate is zero. On day four, you see a shadow failure: the new handler returned 422 on a customer.subscription.updated event where the previous plan had been deleted. The old handler had a guard for this case that was not ported to the new one.
You fix the missing guard. The staging handler gets a new deploy. Shadow failures drop to zero again.
Day 12: You have seen 12,000 subscription events processed by the shadow handler with zero failures. Including several of the rare edge cases — paused subscriptions, promotional discounts, grandfathered billing plans. All correct.
Day 13: You update the hook's primary target to the new handler. Remove the shadow target. Switch complete. Zero customer impact. Zero outage. Zero guesswork.
Without shadow delivery, "Day 1 to Day 7" looks different: you either accept the risk and deploy blind, or you build a traffic replay harness yourself, or you delay the migration while you attempt to synthesize more comprehensive test data. The migration takes three times as long and carries more risk when it finally happens.
What You See in the Dashboard
The event detail modal shows shadow delivery results alongside primary delivery results:
- Shadow status: the HTTP status code returned by the shadow target
- Shadow latency: response time in milliseconds
- Shadow error: the error message if the shadow request failed at the connection level (timeout, connection refused, DNS failure)
Shadow results appear in every event that had shadow delivery enabled at the time of receipt. Historical events retain their shadow results. You can browse through events and filter by shadow status to find all shadow failures in a given time window.
The hook detail page shows aggregate shadow statistics: shadow success rate, shadow p50/p95 latency, shadow failure count in the last 24 hours. These aggregate metrics give you the at-a-glance confidence signal you need to decide when the shadow handler has seen enough traffic.
Why This Is a Team Tier Feature
Shadow delivery requires concurrent delivery infrastructure. Every event must be dispatched to two targets simultaneously, with both results captured and associated with the same event record. This is a materially different delivery pipeline from standard single-target delivery.
The infrastructure for concurrent delivery — connection pooling to multiple targets, parallel request dispatch, result aggregation, shadow-specific logging — carries real cost. Team tier and above includes this infrastructure.
Free and Pro tier hooks have a single target. If you are on Pro and need shadow delivery for a migration, the path is to upgrade for the duration of the migration, complete the switch, and return to Pro. The shadow target configuration remains in place after downgrade but shadow delivery stops; you will not lose your configuration.
Comparison
| Capability | HookTunnel | ngrok | Webhook.site | Hookdeck | Svix | |---|---|---|---|---|---| | Shadow (concurrent) delivery to a second URL | Yes | No | No | No | No | | Shadow failures isolated from primary outcome | Yes | No | No | No | No | | Shadow latency tracked per event | Yes | No | No | No | No | | Aggregate shadow success rate in dashboard | Yes | No | No | No | No | | Shadow results in event detail modal | Yes | No | No | No | No | | Primary delivery latency unaffected by shadow | Yes | N/A | N/A | N/A | N/A |
ngrok's inspect tool is built for debugging, not for production canary delivery. It has no concept of shadow delivery, concurrent target dispatch, or production-safe parallel execution. Webhook.site is a capture tool with no forwarding. Hookdeck and Svix support single-target delivery with retry configuration but have no shadow delivery capability.
Edge Cases and How They Are Handled
Shadow target is slower than primary. Shadow requests run on a parallel path. If the shadow target takes 10 seconds to respond and the primary returns in 200ms, the primary outcome is already recorded at 200ms. The shadow result is recorded when it completes. The provider's perspective is determined entirely by the primary delivery.
Shadow target is unreachable. Connection failures on the shadow path are logged as shadow errors (connection refused, DNS resolution failure, timeout). They do not trigger circuit breaker logic for the primary target. They do not affect the hook's delivery success rate. They appear in the event detail with the error type.
Shadow target returns an unexpected response body. The shadow response body is captured and stored (subject to your payload storage configuration). You can inspect what your new handler returned for any specific event, which is useful for debugging business logic divergence even when both handlers return 2xx.
Primary delivery fails while shadow succeeds. Both outcomes are recorded independently. A primary failure triggers circuit breaker counting and retry logic. The shadow success is noted in the event detail. This is useful information — it can indicate that your primary handler has a problem that your shadow handler does not, which may accelerate your timeline for switching.
FAQ
Does the shadow target receive the original headers?
Yes, with two additions. The shadow request includes all headers present on the original webhook from your provider, plus X-HookTunnel-Shadow: true (so your handler can identify shadow traffic) and the standard HookTunnel correlation headers.
Can I configure the shadow target per hook or globally?
Per hook. Each hook has an independent shadow target configuration. You can have different hooks pointing to different shadow targets simultaneously — useful if you are running multiple migrations in parallel.
Does shadow delivery count against my request quota?
Shadow deliveries are counted at a reduced rate against your quota. They represent real infrastructure usage but are distinguishable from primary deliveries in the billing calculation. Team tier has sufficiently high quota that shadow delivery for active migrations is unlikely to approach limits in normal use.
Can I use shadow delivery for load testing?
Shadow delivery is designed for correctness validation, not load testing. If your goal is to load test a new handler, shadow delivery will give you realistic request rate data. But if your shadow target cannot handle production load levels, the connection errors will appear in event details and may skew your confidence signal. Shadow delivery does not throttle, backpressure, or otherwise modify the rate at which events arrive.
How does this interact with Outcome Receipts?
If your shadow handler sends a receipt back to HookTunnel, the receipt is recorded against the shadow delivery outcome, not the primary delivery outcome. This allows you to validate end-to-end outcome tracking in your new handler — confirming it not only returns 2xx but also sends correct receipts with correct outcome fields — before you switch it to primary.
Test your new handler against real production traffic
Shadow delivery sends a silent copy of every webhook to your staging URL — primary delivery is never affected.
Get started free →