Back to Blog
Webhooks

Knock Stopped Retrying Your 404s. Your Status Codes Are Now a Delivery Policy.

On October 8, 2026, Knock rewrote its outbound webhook retries: 301, 302, 303 and 400 through 405 are never retried, 429 honours Retry-After, and the fixed eight attempts became a handful over a few hours. Here is why the codes your stack emits by accident now decide what you lose.

WebhookVault Team · Webhook Infrastructure Experts10 min read
Close-up of a dark terminal screen with download progress and checksum output on the left and an htop process list with CPU and memory bars in a second window on the right

A 404 used to buy you eight more chances

You ship a refactor that renames /hooks/knock to /webhooks/knock. The old route is gone for eleven minutes before someone notices the config still points at it. Under the old rules that was an annoyance. Knock retried any non-2xx response, up to eight times, and once you fixed the URL the backlog drained by itself.

Under the rules Knock published on October 8, 2026, those eleven minutes of 404s are gone. Not delayed. Gone. A 404 is now a terminal answer, and Knock takes you at your word.

Knock's new policy is reasonable, and I'd expect more senders to land in the same place. What should bother you is the side effect. A status code your framework emitted without you ever choosing it now works as an instruction to delete an event. If you receive webhooks from anyone, your error paths deserve a second look.

What Knock changed on October 8

Knock rewrote its outbound webhook documentation on October 8, 2026, in a docs commit titled "update outbound webhook docs for new retry strategy". The old text said Knock would attempt to send the webhook eight times. The new text says three things.

Knock still retries non-2xx responses, with exceptions. Responses of 301, 302 and 303, and 400, 401, 402, 403, 404 and 405, are never retried.

For every other non-2xx code, Knock "retries a handful of times over the span of a few hours." There is no count, no interval and no ceiling.

If you answer 429 with a well-formed Retry-After header, Knock "will do its best to respect the value of that header."

Signing did not change. Deliveries still carry an x-knock-signature header shaped like t=timestamp,s=signature: an HMAC-SHA256 over the millisecond timestamp and the stringified payload, joined by a dot. The docs also repeat the usual advice to acknowledge with a 2xx before doing the work, because a timeout counts as a failure and gets retried.

Hookdeck's webhook-skills repository already opened a fix dropping the old "8 times" claim from its Knock skill. Your runbook probably has the same line.

The codes that now end the conversation

Read the exclusion list the way the sender does. Each code claims that trying again cannot help.

A 400 says the request is malformed, so the same bytes will be rejected again. A 401 or 403 says you aren't authorised, and the sender has no new credentials to offer. A 404 or 405 says the resource or method doesn't exist here. A 402 says payment is required, which no sender fixes by retrying at 3am. The redirects, 301 through 303, say "go somewhere else", and Knock declines to go.

From the sender's side that is sound. Retrying a permanent failure burns their workers and hammers an endpoint that already said no.

That logic assumes your status codes are deliberate. Most of them aren't.

Your stack emits these codes without asking you

Take a normal week on a webhook receiver and count the terminal codes you send without writing a single res.status() call.

During a deploy, the old container drains while the new one isn't registered yet. Your load balancer or ingress answers for the gap, and depending on configuration that answer can be a 404 for a route with no backend. A path rename, a missing trailing slash or a reordered middleware stack gives you real 404s until someone notices.

Move the handler behind a router that only matches PUT and you get 405s.

Your body parser rejects a payload a few kilobytes over its default limit, or with a content type it didn't expect. That is a 400 from a library, logged by nobody.

You rotate your signing secret, deploy the receiver first, and the sender signs with the old one for twenty more minutes. Every delivery in that window fails verification. If your middleware returns 401, you just told the sender to drop all of them. We covered overlapping secrets in our post on rotating after a compromise. Getting it wrong used to cost you time. Now it costs you data.

Then there is whatever sits in front of you. A WAF rule that dislikes a payload field, a bot filter, a geo block on a new sender region: these tend to answer 403. Your application never sees the request, so your application logs never show the loss.

Every one of these is a transient condition wearing a permanent code.

Redirects are the sneakiest of the lot

301, 302 and 303 get their own warning because they look like success when you test by hand.

You move your endpoint to a new domain and redirect the old one. Your browser follows it. curl -L follows it. Your uptime monitor accepts 3xx and stays green. Knock gets a 301, doesn't follow it, and doesn't retry.

Same story with an HTTP-to-HTTPS redirect on a receiver registered with an http:// URL years ago, or a framework that redirects /webhooks/knock to /webhooks/knock/ because of a trailing-slash setting. It works in every tool you check it with and fails in the one client that matters.

The fix is boring. Register the exact final URL with the sender and make webhook paths answer directly. Then test with a client that doesn't follow redirects, since that is what you are actually serving.

A retry schedule you cannot see is a window you cannot plan around

"Eight attempts" is a number you can do arithmetic with. You can estimate how long your receiver can be down before events fall off the end, and write that into a runbook. "A handful of times over the span of a few hours" has no unit. It could be four attempts over two hours this month and six over five hours next quarter, and nothing obliges anyone to tell you.

Senders have their reasons. An undisclosed schedule lets them tune for load and change strategy without a changelog entry or a migration period. The burden lands on you, though. If you can't read your survivable outage window off the docs, you can measure it yourself or stop depending on it.

Do both. We wrote about what happens after retries run out in When the Sender Gives Up on Your Webhook Endpoint. With an opaque schedule you just find out later.

Pick your status codes on purpose

Decide which response every failure path produces, and keep terminal codes for failures that really are terminal.

My rule: if a human could fix the cause and the same delivery would then succeed, it isn't terminal. Return 503 and let the sender come back. Return a 4xx from the exclusion list only when you have decided you never want this delivery again.

app.post('/webhooks/knock', express.raw({ type: '*/*', limit: '5mb' }), async (req, res) => {
  const verdict = verifyKnockSignature(req)

  if (verdict === 'secret-mismatch') {
    // Could be a rotation in progress. Ask for a retry instead of a drop.
    log.warn({ reason: 'signature-mismatch' }, 'webhook rejected, retryable')
    return res.status(503).set('Retry-After', '120').end()
  }

  if (verdict === 'malformed-header') {
    // Genuinely broken request. Terminal on purpose.
    return res.status(400).end()
  }

  await queue.enqueue('knock-event', req.body)
  return res.status(202).end()
})

// Catch-all for anything under /webhooks that did not match a route.
app.all('/webhooks/*', (req, res) => res.status(503).set('Retry-After', '300').end())

People skip the catch-all. They shouldn't. A path mismatch during a deploy should look different from "this endpoint will never exist". Same for your proxy: configure the ingress so "no healthy backend" returns 503.

Answering 503 on a signature mismatch is a judgement call. A forged request gets retried a few times before it goes away, which costs you a handful of extra rejections. That's cheap next to losing every real event during a rotation. Save the 4xx path for requests that are malformed beyond doubt.

429 with Retry-After is the only brake pedal you get

If a burst of deliveries is swamping your consumer, the docs give you one explicit control: answer 429 with Retry-After. Knock says it will do its best to respect it. That's softer than a guarantee, and it's still the only documented lever that shapes the schedule.

Use it for backpressure. When queue depth crosses a threshold, start answering 429 with a delay that matches how fast you expect to drain. Be honest about that delay. Retry-After: 3600 because you'd like a quiet hour may push the next attempt past whatever "a few hours" means today, and you're back to losing events.

For the receiver side of throttling, token buckets included, see our earlier piece on rate limiting webhooks.

Measure the schedule from your own logs

The docs no longer tell you the schedule. Your traffic does. Log every inbound attempt, including the ones you reject, with a timestamp and a hash of the raw body. Retries of one delivery should carry the same payload even when the signature timestamp changes, so the body hash is a reasonable grouping key. Check that against your own traffic before relying on it.

SELECT body_sha256,
       count(*)                          AS attempts,
       min(received_at)                  AS first_attempt,
       max(received_at)                  AS last_attempt,
       max(received_at) - min(received_at) AS span
FROM webhook_attempts
WHERE source = 'knock'
  AND received_at > now() - interval '30 days'
GROUP BY body_sha256
HAVING count(*) > 1
ORDER BY span DESC
LIMIT 50;

The longest spans are the closest thing you have to a published window. Rerun it monthly. If the numbers shift, the sender changed something.

Two conditions apply. You have to log attempts before any middleware can reject them, usually at the proxy or the first line of the handler. And you have to log rejections at all, which most receivers skip because a 4xx feels like the sender's problem.

Getting back what a terminal code threw away

Say the damage is done. You returned 404 for eleven minutes and those deliveries won't come back on their own.

Knock's outbound webhook docs don't describe a resend or replay operation, so don't plan around one. Recovery has to come from state you can query: the provider's API, or your own record of what you expected to receive. We made that case in The Webhook You Never Got Is Sitting in an API You Forgot to Call. Webhooks notify you. The system of record lives somewhere else, and a receiver that only learns about the world through webhooks can't repair a gap.

Your attempt log at least tells you which window was lost.

Treat the next changelog line as a contract change

Every sender you depend on has a retry policy, and it is part of your reliability whether you ever read it or not. When a sender narrows what it retries, or stops publishing its schedule, risk moves to your side of the wire while your code stays exactly the same.

Keep an inventory. Per provider, write down which codes it treats as terminal, whether it honours Retry-After, and your measured retry span. Then audit the receiver against that list, including every path owned by your proxy, your WAF and your framework. Most teams that try this find a route that has been answering 404 during deploys for months.

Frequently asked questions

Should a webhook receiver ever return 4xx? Yes, when you have decided the delivery should never come back, such as a request that is malformed beyond any doubt. If the cause is something you or the sender could fix, like a rotation in progress, a deploy gap or a temporary overload, return 503 or 429 so the event gets another chance.

Does returning 503 for a bad signature help an attacker? Not in any meaningful way. A forged request is still rejected on every attempt and never reaches your business logic. The sender may retry it a few times, which is a small amount of extra traffic compared to silently dropping real events during a secret rotation.

Will Knock follow a 301 if the new URL is on the same domain? The documentation lists 301, 302 and 303 as responses Knock does not retry and says nothing about following them, so assume a redirect ends the delivery wherever it points. Register the final URL directly and make sure your receiver answers webhook paths without redirecting.

How long can my endpoint be down before Knock events are lost? Knock no longer publishes a number, only that it retries a handful of times over a few hours for retryable codes. Measure the real span from your own attempt logs, treat the shortest window you observe as your budget, and keep a reconciliation path for anything that falls outside it.

Related posts