When My Contact Form Plugin Sent Duplicate Emails Due to Retry Logic and the Idempotency Fix That Stopped the Flood

It all started with a gentle ding. Then another. Then ten more. Like popcorn in a microwave, our team inbox started *popping*. Something was clearly wrong. My contact form was going haywire, sending duplicate emails like there was no tomorrow.

What followed was a deep dive into retry logic, a lesson in building *idempotent* systems, and many apologies to our marketing team. Buckle up—this is the story of how retry logic filled our inbox, and how we stopped the email tsunami.

TLDR:

  • My contact form plugin sent multiple duplicate emails.
  • The problem? Every time a request failed or timed out, the system retried it…
  • …but each retry re-sent the same email!
  • The fix? Add idempotency, so the same request can be retried without side effects.

The Calm Before the Storm

Our website had a nice little contact form. Simple. Elegant. Visitors typed in their message, clicked “Send,” and we’d get a neat email in our inbox.

Or so we thought.

Everything worked great—until one day, we launched a new marketing campaign that brought a flood of new visitors. That’s when our inbox exploded. Each submitted message came through… threefourfifteen times.

Our poor email system was gasping for air.

The Unexpected Villain: Retry Logic

Let’s rewind. To make our form more robust, I had enabled retry logic in our backend. The idea was simple:

  • If sending the message fails (due to a network blip, timeout, or anything else), try again.

Seems smart, right?

The problem was, each retry acted like a whole new request. No memory. No awareness that it was a repeat. So every time it retried, it resent the email. Over and over.

This turned one message into five, ten, or more—even though to the user, they only clicked “Send” once.

Why This Happens: Stateless Requests

Web systems are usually stateless. That means the server doesn’t remember what happened five seconds ago. Each time a client (like your browser or app) sends a request, the server treats it as totally new.

If retry logic is involved—and most APIs and plugins have this—it means the same message might be sent many times if anything goes wrong.

Let’s say a network glitch happens right after the form sends. The server already got the message and sent the email, but the plugin thinks something failed because it didn’t get a reply. So it tries again. And again.

Each of those retries? Another email.

The Wake-Up Call

We started noticing weird things in our inbox:

  • Same user, same message, five copies in a row.
  • Different timestamps a few seconds apart.
  • Interns screaming “Make it stop!”

At first, we thought it was a spammer. Then we blamed bots. Then we looked at our logging system—and everything clicked into place.

Enter Idempotency: Our Hero

You know what takes retry logic from chaos to calm?

Idempotency.

It sounds fancy, but it’s actually easy to understand. An idempotent operation is one where doing it many times gives the same result as doing it once.

Think of a light switch. Turning the light ON five times doesn’t make the room brighter. It just stays ON.

We needed to make our contact form logic work the same way. Even if the same request came in 10 times, it should result in one email being sent. Not 10 duplicates.

Implementing the Idempotency Fix

Here’s what we did:

  1. When a user submits the form, we generate a unique message ID—like a fingerprint for that submission.
  2. Each backend request includes this ID.
  3. Before sending an email, we check: “Have we already seen this message ID?”
  4. If yes, skip it. If no, send the email and store the ID in our database (with a timestamp).

Fairly simple. Super effective.

This tiny bit of memory turned our system from wild to wise. Now, duplicates still come in if there’s a retry—but they get quietly dropped.

Other Tricks We Used (Just In Case)

  • We limited retry attempts to 3.
  • We added more logging to trace each form submission.
  • We updated our alert system to ping us if more than 5 emails come in from the same IP in a minute.

But really, the idempotency key was the game-changer.

Lessons We Learned (The Hard Way)

This whole experience taught us a few things that are useful for any dev team—or solo web creators:

  • Always assume retries will happen. You can’t control the network; failures and timeouts are normal.
  • Stateless systems need memory elsewhere. Your server might forget, but your database can help remember past requests.
  • Add idempotency where you can. It’s especially important for things with side effects: emails, texts, payments, etc.
  • Test like you’re on a flaky Wi-Fi connection. Simulate timeouts. Pull the plug. See what breaks.

Would It Have Happened with Stripe?

Nope. That’s because companies like Stripe, Twilio, and others already use idempotency keys. In fact, Stripe even documents it clearly.

When charging a card, if the request times out, a retry with the same idempotency key won’t double-charge the card.

We needed to think the same way, even though we were just sending emails. Because guess what? To your clients, a dozen identical replies looks almost as bad as double-charging them.

The Peace After the Flood

Now that the fix is in place, our inbox is calm again. Each message shows up precisely once. Interns are happy. The marketing team is dancing.

More importantly, we’re ready. Next time something glitches, we won’t wake up to 90 unread emails all saying, “Hi, I’d like a quote.”

That’s the power of building with care—and not forgetting that the web is a weird, failure-prone place.

Image not found in postmeta

In Case You Wanna Build It Too

Here’s a quick snippet (pseudo-code style) to illustrate:

message_id = generate_uuid()

if not database.has_seen(message_id):
    send_email(to=recipient, message=body)
    database.record(message_id, timestamp)
else:
    log("Duplicate message—email skipped.")

That’s really it. One simple check, and the duplicate nightmare is over.

And They Lived Happily Ever After

This bug taught us to always expect the unexpected. More importantly, it taught us that retry logic isn’t bad—just misunderstood. When paired with idempotency, retries become your friend, not your enemy.

So if you’re building a form, an API, or anything that talks to the outside world—give it a memory. Future you will thank you.

Now go forth, and stop your systems from spamming yourself. 👋