# What happens when a form webhook fails

Your endpoint was down, slow or moved. How 10 form backends retry a failed webhook, what they sign, and how to stop a retry making a duplicate lead.

Author: Formgong
Published: 2026-10-31
Language: en
Canonical: https://formgong.com/en/blog/form-webhook-fails/

When a form webhook fails, most form backends send the same submission again later, but how persistent they are varies a lot: Splitforms tries once more after about 2 seconds, Basin up to 15 times over 24 to 28 hours. Formgong makes 5 signed attempts over about 42 minutes on every plan, then alerts you. A retry can arrive after your code already saved the lead, so store the submission id and skip repeats.

## When a webhook counts as failed

You set up a webhook so each form submission lands in your CRM or your own database. Then one lead is missing there, though it is in the form inbox. Somewhere between the two, the POST failed.

A delivery fails when your endpoint answers with anything outside 200 to 299, takes longer than the sender is willing to wait, or cannot be reached at all. A redirect counts as a failure too. Formgong and Formspark do not follow redirects, so if your URL moved from `http` to `https`, or your framework answers a missing trailing slash with a 301, every delivery fails while the address looks right.

What happens next depends on the service. Some keep trying for a day, and some give up after two seconds.

## How form backends retry a failed webhook

Webhook delivery as each service describes it in its own docs on 31 October 2026. "Not stated" means we could not find it there.
ServiceWebhooks onWhen a delivery failsWaits for an answerSignedId to skip repeats
FormgongEvery plan, 3 per form on Free5 attempts over about 42 minutes10 sYes, every request`submission.id` in the body
[Formspree](/en/formspree-alternative/)Professional and BusinessUp to 10 retries, schedule not stated3 sNot statedNot stated
[Basin](/en/basin-alternative/)Growth and aboveUp to 15 over 24 to 28 hoursNot statedYesNot stated
[Formspark](/en/formspark-alternative/)Free and paidEvery 5 minutes until it works, paused after about a day5 sOptional, with a timestampSubmission id header
[Forminit](/en/forminit-alternative/)ProBackoff, number not statedNot statedYes, with a timestampDelivery id header
[Static Forms](/en/staticforms-alternative/)Starter and aboveUp to 3 attemptsNot statedYes, with a timestampDelivery id header
[Splitforms](/en/splitforms-alternative/)Paid plans1 retry about 2 s later, none for 4xx8 sYesNot stated
[Web3Forms](/en/web3forms-alternative/)ProAutomatic, number not stated30 sNot statedNot stated
[Netlify Forms](/en/netlify-forms-alternative/)Not statedNot statedNot statedNot statedNot stated
[FormSubmit](/en/formsubmit-alternative/)FreeNot statedNot statedNot statedNot stated
The spread matters when your server is down for a deploy or a few hours. Basin's 15 tries across a day, or Formspark's every 5 minutes, ride out a long outage. Splitforms' single retry two seconds later covers a dropped connection and not much more, and it does not retry a 4xx at all. Formspark also keeps the order: later submissions wait behind the failed one, and after about a day of failures it pauses and emails the workspace admins.

Formspark, Forminit and Static Forms put a timestamp into the signature and send an id you can use to skip a repeat. Basin and Formgong sign the body only, so your duplicate check also has to catch a replayed old request. Formspree, Web3Forms, Netlify Forms and FormSubmit say nothing about signing on their webhook pages.

Where you see it: Basin, Forminit, Static Forms and Splitforms keep a delivery log with a button to send a submission again. Forminit and Static Forms email you about failures; Static Forms does so after 24 hours of failed deliveries.

## What Formgong does when your endpoint fails

The first attempt goes out right after the submission is saved. If it fails, Formgong waits 30 seconds, 2 minutes, 10 minutes and 30 minutes before the next ones. That is 5 attempts over about 42 minutes, plus up to 10 seconds for each attempt your endpoint leaves hanging. The waits are fixed, and a `Retry-After` header does not change them.

Any answer outside 200 to 299 is a failure, a 404 and a redirect included, and so is no answer within 10 seconds. Each attempt is a new request carrying the same submission id. After the last one the delivery is marked failed and you get one alert, in your linked Telegram chat or by email if there is none, at most once a day per form and channel. The submission stays in your inbox the whole time.

The form's webhook settings show **Recent deliveries**: time, OK or ERR, the status code and the error. Each submission lists its deliveries with the number of attempts and the last error, and a failed one gets a **Retry** button for when your endpoint is fixed. The **Test** button sends a `webhook.test` event, so you can check a new URL before a real lead depends on it.

A wrong address shows up in the log. Formgong will not save an `http://` or private address, and before every attempt it checks that the host resolves to a public IP, so a typo in the domain reads as a DNS error there. It is retried like any other failure: fix the URL, then press Retry. The same rule is why the test below ran a copy of the request code on a laptop: Formgong refuses to send to `localhost`.

## We broke a receiver on purpose

We wanted to see a failed webhook from the receiving end, so we wrote a small Node server that fails on command and a script that sends one signed submission the way Formgong's queue does: the same JSON, the same `X-Signature`, `X-Formgong-Event` and `user-agent` headers, a 10-second timeout, no redirects, and the real waits from the code. Everything ran on 127.0.0.1 in real time on 31 October 2026; nothing went to Formgong's servers.

If your endpoint is down for a deploy, the fourth attempt arrives almost 13 minutes after the submission, and the fifth and last one after 42 and a half. Every request carried the same submission id, but with file links turned on each body and signature was different, because the download links are signed fresh on every attempt.

The third case matters most: a receiver that did its work but answered late got the same lead twice.

```text
1. Endpoint answers 500, 500, then hangs (file links on)
   0:00  attempt 1   HTTP 500
   0:30  attempt 2   HTTP 500
   2:30  attempt 3   no answer, gave up after 10 s
  12:40  attempt 4   200, lead saved
  receiver saw sub_7f3a 4 times, with 4 different bodies and signatures

2. Nothing listening on the port
   0:00  attempt 1   ECONNREFUSED
   0:30  attempt 2   ECONNREFUSED
   2:30  attempt 3   ECONNREFUSED
  12:30  attempt 4   ECONNREFUSED
  42:30  attempt 5   ECONNREFUSED -> marked failed, owner alert

3. Receiver saves the lead, then answers after 12 s
   0:10  attempt 1   gave up after 10 s
   0:40  attempt 2   200
  without a duplicate check: 2 rows in the CRM
  with a duplicate check:    1 row, the second request skipped
```

## Why a retry can create a duplicate lead

The sender only knows what it saw. In the third case the receiver saved the lead and then took 12 seconds to answer. Formgong gave up at 10 seconds, counted a failure and sent the submission again 30 seconds later. From its side that is the right call. From yours it is a second row in the CRM and a second email to the sales team. Formspree's docs warn about the same thing: a retried delivery can arrive more than once.

The opposite mistake loses leads. If you answer 200 first and the save fails afterwards, nobody retries, because a 2xx means done. So save the lead before you answer, and start the slow work after.

Do not try to spot repeats by hashing the body or comparing signatures. That works until you turn on file links, and then every attempt looks new. Use `submission.id`, which stays the same across attempts and across a manual Retry.

## A receiver that survives retries

- **Check the signature first.** Read the raw body and compare its HMAC with `X-Signature` before you parse anything. Answer 401 if it does not match. The full check in Node, Python, PHP and Workers is in [verify a form webhook signature](/en/blog/verify-form-webhook-signature/).

- **Answer 200 to events you do not handle**, such as `webhook.test`, so tests and future event types never look like failures.

- **Save `submission.id` under a unique key**, in the same write as the lead. If the key is already there, it is a retry: answer 200 and stop. A unique column in the database does this better than a check in code, because two attempts can overlap.

- **Answer within a couple of seconds.** Put CRM calls and emails after the answer or on your own queue.

- **Answer 500 or 503 when you could not save it.** That is how you ask for another attempt.

- **Paste the final URL**: `https`, the exact path, nothing that redirects.

- **Down for more than about 42 minutes?** Fix the endpoint, then press Retry on each failed delivery. The duplicate check makes that safe even if some of them did arrive.

Here is a whole receiver doing that with no packages. We ran it against the same replay script: two deliveries of the same submission left one row, and a request with a forged signature got a 401.

```javascript
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
import { DatabaseSync } from "node:sqlite";

const SECRET = process.env.FORMGONG_WEBHOOK_SECRET;
const db = new DatabaseSync("leads.db");
db.exec("CREATE TABLE IF NOT EXISTS leads (submission_id TEXT PRIMARY KEY, email TEXT, received_at TEXT)");
const insert = db.prepare("INSERT INTO leads VALUES (?, ?, ?) ON CONFLICT(submission_id) DO NOTHING");

function signed(raw, header) {
  if (!/^sha256=[0-9a-f]{64}$/.test(header ?? "")) return false;
  const want = Buffer.from("sha256=" + createHmac("sha256", SECRET).update(raw).digest("hex"));
  return timingSafeEqual(Buffer.from(header), want);
}

createServer(async (req, res) => {
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  const raw = Buffer.concat(chunks);
  if (!signed(raw, req.headers["x-signature"])) return res.writeHead(401).end();
  const event = JSON.parse(raw.toString("utf8"));
  if (event.event !== "submission.created") return res.writeHead(200).end();
  try {
    const { submission } = event;
    const { changes } = insert.run(submission.id, submission.fields.email ?? null, new Date().toISOString());
    if (changes === 0) return res.writeHead(200).end("already have it");
    res.writeHead(200).end("ok");                  // answer as soon as the row is saved
    setImmediate(() => notifySales(submission));   // slow work after the answer
  } catch (error) {
    console.error(error);
    res.writeHead(503).end();                      // not saved: ask Formgong to try again
  }
}).listen(3000);

function notifySales(submission) {
  console.log("new lead", submission.id);
}
```

## Did your endpoint fail in a way we missed?

If your webhooks broke in a way this article does not cover, or a vendor's docs say something different now, write to [support@formgong.com](mailto:support@formgong.com). We read every message and fix the article when we are wrong.

## What Formgong's free plan includes

- **Build the form or bring your own.** The [form builder](/en/tools/form-builder/) takes the colours and fonts from your site's address, with no sign-up; ready [form templates](/en/templates/) copy into your account; AI writes the fields from one sentence. Any hand-written HTML form works too.

- **Websites with the form already connected.** 28 free [website templates](/en/website-templates/) post to Formgong out of the box.

- **250 submissions a month on unlimited forms.** Spam does not count. Once a year, a busy month keeps delivering on credit until the 7th instead of stopping.

- **Everywhere you work.** Email to up to 3 people at once, [Telegram](/en/integrations/telegram/) with Reply and Handled buttons, [Google Sheets](/en/integrations/google-sheets/), [Notion](/en/integrations/notion/), and 3 signed [webhooks](/en/integrations/webhooks/) for Slack, Discord, Zapier, Make or n8n.

- **Emails to your customers in your business's name.** Formgong sends them under your business name and replies come back to you, with no email service of your own to set up: a review request to every lead, a reminder a visitor asked for, and review emails after an order with your logo and colours. People who agree can also go straight into your [Mailchimp, Klaviyo, Brevo, MailerLite or Kit](/en/docs/marketing-lists/) list. An automatic reply to each visitor is on Pro.

- **No captcha needed.** A server-side spam filter, a honeypot, allowed domains and a block list; add your own Turnstile key if you want one.

- **Files and voice.** Visitors can attach files, or record a [voice message](/en/docs/voice-field/) that arrives transcribed.

- **Made for AI-built sites.** A hosted form page and a contact button; step-by-step guides for sites built with [Lovable](/en/docs/lovable/), [Bolt](/en/docs/bolt/), [v0](/en/docs/v0/), [Replit](/en/docs/replit/) and [Base44](/en/docs/base44/); an [MCP server](/en/docs/mcp/) lets Cursor or Claude wire the form for you.

- **See what the form does.** [Analytics](/en/docs/analytics/) for views, starts, conversion and where each lead came from, and an alert when a form goes quiet.

- **One account for the rest.** Customer reviews and an [AI help chat](/en/docs/help-chat/) that answers visitors from your own pages, with every message in one inbox. Data is stored in the EU.

## Frequently asked questions

### How many times does Formgong retry a failed webhook?

5 attempts in total, with waits of 30 seconds, 2 minutes, 10 minutes, 30 minutes between them, about 42 minutes. After the last one you get one alert in Telegram or by email, and the submission gets a Retry button.

### Does Formgong retry a 404 or a redirect?

Yes. Any answer outside 200 to 299 counts as a failure and is retried until the last attempt, redirects included, because Formgong does not follow them. Fix the URL, then press Retry on the failed deliveries.

### How do I stop a retried webhook from creating a duplicate?

Store submission.id under a unique key in the same write as the lead. When the key already exists, answer 200 and do nothing. Do not compare body hashes: with file links on, each attempt has a different body.

### How long does a form backend wait for my webhook to answer?

Formgong waits 10 seconds. From their own docs on 31 October 2026: Formspree 3, Formspark 5, Splitforms 8 and Web3Forms 30 seconds. Basin, Forminit, Static Forms, Netlify Forms and FormSubmit do not say.

### Are form webhooks signed on free plans?

Formgong signs every webhook on every plan, with 3 webhooks per form on Free. Formspark has webhooks on its free plan with optional signing. Basin, Forminit, Static Forms and Splitforms sign, but their webhooks are on paid plans only.

## Sources and documentation

- [Formspree: Webhooks](https://help.formspree.io/articles/plugins/webhooks.md)
- [Basin: Webhooks](https://docs.usebasin.com/integrations/webhooks/)
- [Formspark: Webhooks](https://documentation.formspark.io/integration/webhooks.md)
- [Forminit: Webhooks](https://forminit.com/docs/webhooks/)
- [Static Forms: Verify webhook signatures](https://www.staticforms.dev/docs/integrations/webhooks/verify-signatures)
- [Static Forms: Webhook retries and idempotency](https://www.staticforms.dev/blog/webhook-retry-idempotency)
- [Splitforms: Webhooks](https://splitforms.com/features/webhooks)
- [Web3Forms docs](https://docs.web3forms.com/llms-full.txt)
- [Netlify: Form notifications](https://docs.netlify.com/manage/forms/notifications.md)
- [FormSubmit documentation](https://formsubmit.co/documentation)
- [Formgong webhooks](/integrations/webhooks)
- [Formgong webhook docs](https://formgong.com/en/docs/webhooks/)

[Get a form key](https://formgong.com/en/#top)
