A webhook receiver that deliberately delays its response, returns chosen error codes, rate-limits traffic, or fails a set number of times before succeeding takes only a short script to build. What it lets you observe is how a sender reacts to those conditions. It cannot tell you a universal timeout or retry rule, because those belong to each provider, and you have to read the behavior from that provider’s documentation or its delivery logs.
What the receiver controls and what the sender controls
Before you simulate anything, separate the two sides of a webhook exchange. Your receiver decides how long it waits and what it returns. The sending provider decides how long it waits for you, which status codes count as failures, and how often it tries again.
As an Amazon Associate I earn from qualifying purchases.
| Behavior | Who sets it | Where to verify it |
|---|---|---|
| Response delay | Your receiver | Your code and logs |
| Status code returned (200, 500, 503, 429) | Your receiver | Your code and logs |
| Retry-After header | You send it; whether the sender honors it is provider behavior | The provider’s documentation and attempt history |
| Timeout threshold | Sender | Provider settings or documentation. Twilio’s test tool compares result timing with configured connection and read timeouts. |
| Retry count and schedule | Sender | Provider documentation. Stripe says it retries several times but does not publish a fixed count in the cited troubleshooting article. |
| Which status codes trigger a retry | Sender | Provider documentation |
The practical consequence: a timeout you see in a sender’s log is a fact about that sender’s settings, not about your code. Tune your receiver’s delay against the provider’s timeout, not against a number you assumed.
The five failure shapes worth simulating
Each failure shape tests a different part of the sender’s behavior. Simulate them one at a time, so a result can be traced to a single cause.
#1 Best Overall
- Includes Raspberry Pi 5 with 2.4Ghz 64-bit quad-core CPU (8GB RAM)
- Includes 128GB Micro SD Card pre-loaded with 64-bit Raspberry Pi OS, USB MicroSD Card Reader
- CanaKit Turbine Black Case for the Raspberry Pi 5
- CanaKit Low Noise Bearing System Fan
- Mega Heat Sink - Black Anodized
Delayed response
Hold the response for a fixed number of milliseconds before replying with a success code. This tests whether the sender gives up before your receiver answers. If the delay is longer than the sender’s timeout, the sender records a failure even though your handler eventually returned 200.
Server error (500)
Return a 500 for every request, or for a set count of requests. This shows whether the sender retries a generic server failure and how quickly its retries arrive.
Rank #2
- Includes Raspberry Pi 4 4GB Model B with 1.5GHz 64-bit quad-core CPU (4GB RAM)
- Includes Pre-Loaded 32GB EVO+ Micro SD Card (Class 10), USB MicroSD Card Reader
- CanaKit Premium High-Gloss Raspberry Pi 4 Case with Integrated Fan Mount, CanaKit Low Noise Bearing System Fan
- CanaKit 3.5A USB-C Raspberry Pi 4 Power Supply (US Plug) with Noise Filter, Set of Heat Sinks, Display Cable - 6 foot (Supports up to 4K60p)
- CanaKit USB-C PiSwitch (On/Off Power Switch for Raspberry Pi 4)
Temporary unavailability (503 with Retry-After)
Return 503 with a Retry-After header in seconds. This models a receiver that is overloaded or in maintenance. Whether the sender waits the indicated time, retries sooner, or ignores the header is a provider-specific behavior that you must check in its logs or documentation.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rate limiting (429 with Retry-After)
Count requests in the current window and return 429 once a limit is passed, with a Retry-After header. Test whether the sender backs off after a 429 and whether it keeps every event or drops some. Provider documentation should describe this; do not assume it.
Transient failure followed by recovery
Fail the first N requests, then return 200. This is the most useful shape for checking retries end to end. It confirms that a delivery that eventually succeeds is marked as successful on the sender’s side, and it shows how many attempts were needed.
A minimal receiver you can reconfigure without editing code
The script below uses Node.js’s built-in http module, so it needs no packages. Behavior is set with environment variables, so you can change the failure shape by restarting the process. The code does not verify webhook signatures; a real receiver must do that using the method in each provider’s documentation.
Rank #4
- Broadcom BCM2711, quad-core Cortex-A72 (ARM v8) 64-bit SoC @ 1. 5GHz
- 2. 4 GHz and 5. 0 GHz IEEE 802. 11b/g/n/ac wireless LAN, Bluetooth 5. 0, BLE
- 2 × USB 3. 0 ports, 2 x USB 2. 0 Ports
- 2 × micro HDMI ports supproting up to 4Kp60 video resolution
- Micro SD card slot for loading operating system and data storage
const http = require('http');
const PORT = Number(process.env.PORT || 8080);
const DELAY_MS = Number(process.env.DELAY_MS || 0);
const FAIL_FIRST = Number(process.env.FAIL_FIRST || 0);
const FAIL_STATUS = Number(process.env.FAIL_STATUS || 503);
const RETRY_AFTER = Number(process.env.RETRY_AFTER || 0);
let attempts = 0;
const server = http.createServer((req, res) => {
let size = 0;
req.on('data', (chunk) => { size += chunk.length; });
req.on('end', () => {
attempts += 1;
const n = attempts;
console.log(new Date().toISOString(), req.method, req.url, 'request', n, 'bytes', size);
setTimeout(() => {
const headers = { 'Content-Type': 'application/json' };
if (n <= FAIL_FIRST) {
if (RETRY_AFTER > 0) headers['Retry-After'] = String(RETRY_AFTER);
res.writeHead(FAIL_STATUS, headers);
res.end(JSON.stringify({ error: 'simulated failure', request: n }));
return;
}
res.writeHead(200, headers);
res.end(JSON.stringify({ ok: true, request: n }));
}, DELAY_MS);
});
});
server.listen(PORT, () => console.log('listening on', PORT));
To produce a receiver that waits three seconds, rejects the first two requests with 503 and a five-second Retry-After, then succeeds:
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →DELAY_MS=3000 FAIL_FIRST=2 FAIL_STATUS=503 RETRY_AFTER=5 node flaky-receiver.js
Two details to keep in mind. The request counter is global, so every sender that hits the endpoint shares it, and restarting the process resets it. The Retry-After value is sent in seconds; HTTP also allows an HTTP-date form, which this script does not produce.
Best Value
- Includes Raspberry Pi 5 16GB with 2.4Ghz 64-bit quad-core CPU (16GB RAM)
- Includes 128GB Micro SD Card pre-loaded with 64-bit Raspberry Pi OS, USB MicroSD Card Reader
- CanaKit Turbine Black Case for the Raspberry Pi 5
- CanaKit Low Noise Bearing System Fan
- Mega Heat Sink - Black Anodized
Making a provider reach a local receiver
Most providers cannot call localhost. You need a public HTTPS address that forwards to your local port. Twilio’s guide to testing webhooks describes a local setup that uses a public tunnel, and it states plainly: “To create these tunnels, use ngrok.” (Twilio, Test webhook delivery, local testing section.)
- Start the receiver on a local port, for example
PORT=8080 node flaky-receiver.js. - Start a tunnel that forwards a public HTTPS address to
localhost:8080. Twilio’s guide names ngrok for this. - Copy the HTTPS address the tunnel prints and enter it as the endpoint in the provider’s test or webhook configuration.
- Trigger one test event and check the receiver’s console output before you trust the result.
Some tools require HTTPS even when the endpoint is on your own machine. PayPal’s simulator posts mock events to a listener on HTTPS port 443, so a plain HTTP localhost address will not work there.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Comparing provider test tools
Test tools differ in where the request comes from, which failures they can produce, and what they let you inspect. The table covers the tools discussed in this article; it does not rank them.
| Tool | Request source | Failure responses you can simulate | What you can observe | Endpoint location |
|---|---|---|---|---|
| PayPal Webhooks simulator (PayPal Developer) | Queued mock events for demonstration and listener validation | Not stated; the page describes failed connection or delivery, not controlled error codes | Event status is updated with error details when a connection or delivery fails. PayPal says a queued event often arrives within a minute; this is an operational estimate, not a guarantee. | HTTPS listener on port 443 |
| Twilio Test webhook delivery (Twilio) | One webhook sent to a chosen URL using a named Webhook Setting | Not stated; the feature is marked Public Beta and may change | Result timing, compared with configured connection and read timeout values | Public URL, including a local receiver behind a public tunnel |
| Jitterflow Webhook Failure Simulator (Jitterflow) | A hosted bin URL that receives your sender’s requests | 200; 500; 503 with Retry-After; 429 with Retry-After; failure that recovers to 200 | The bin’s log. This is the vendor’s own description of the tool, not an independent evaluation. | Hosted by the vendor |
| Custom receiver (the script above) | Whatever sender you point at it | Any delay, status code, Retry-After value, or failure count you code | Your own console and logs, including the exact body size and request number you choose to record | Wherever you run it, behind a tunnel if local |
Diagnosing from the provider’s side
Your receiver’s log shows only what arrived. To see what the sender recorded, use the provider’s delivery view. Stripe’s troubleshooting article says Stripe retries several times when an event cannot be delivered, and it recommends inspecting the endpoint’s failed events and each attempt’s status, including the HTTP status and response details (Stripe Support). Compare the attempt list with your receiver’s log. If Stripe shows three attempts and your log shows two, a request either never reached the receiver or was rejected before your handler ran.
Keep test data synthetic
A hosted endpoint is only as private as its URL. Jitterflow warns that anyone who has the bin URL can read its log, and it says to send only test data. The same rule applies to any receiver you expose through a tunnel:
Quick Recap
- Use generated payloads with fake identifiers and amounts.
- Do not send live customer data, personal information, or production secrets in headers.
- Stop the tunnel when you finish, so the public address no longer forwards to your machine.
Troubleshooting
- The sender logs a timeout, but your receiver logged a 200. Your delay is probably longer than the sender’s timeout. Compare DELAY_MS with the timeout configured for that sender; Twilio’s test result shows the timing against its connection and read timeouts.
- Nothing reaches the receiver. Confirm the tunnel is still running and that the endpoint in the provider’s configuration matches the current public address. If your tunnel issues a new address each time it starts, update the endpoint each session.
- The sender retries more often than you expected. The retry schedule is set by the provider. Read its documentation or attempt history rather than inferring the schedule from your own server.
- The sender ignores Retry-After. Whether a sender honors that header is provider-specific. Check the attempt timestamps in the provider’s log to see the actual gap between attempts.
- The failure never stops. The request counter is global and only resets when the process restarts. Restart the receiver to begin a new run.
- A PayPal simulated event does not appear. Check the event’s status in the simulator. PayPal updates it with error details when a connection or delivery fails.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




