4 min read
Making a flaky webhook safe to retry
My contact form saves to a Google Sheet through Apps Script. The first real test timed out after saving the row, which is exactly when a naive retry creates duplicates.
The contact form on this site doesn't talk to a database. A Next.js server action forwards each message to a small Google Apps Script web app, and the script appends a row to a Google Sheet with a Status column I use as an inbox. No servers, no bill, and I can triage messages from my phone.
It worked perfectly against a local mock. Then I pointed it at the real script and measured:
| Call | Result |
|---|---|
| Wrong secret, 5 tries | 4 × {"ok":false} in 2–28 s, 1 × a Google 404 page |
| Form submit, 10 s timeout | timed out at 10.4 s |
| Form submit, 45 s timeout | Google 404 page after 35 s |
| Direct call, right secret | {"ok":true} in 3.1 s |
Slow is annoying. The 404 is worse, because of when it happens.
Why a plain retry is dangerous
An Apps Script web app runs doPost first and answers afterwards. By the time my server sees a timeout or that 404 page, the row may already be in the sheet and the notification email may already be sent.
So "the request failed" doesn't mean "nothing happened". If the visitor clicks Send again, or my server retries on their behalf, I get two rows and two emails for one message. The fix isn't a longer timeout. It's making a second attempt harmless.