Logo
Logo
Home
Archive
AI Agent Notes
Advertise
YouTube
Login
Sign Up
Logo
  • Home
  • Posts
  • 🦥Idempotency for Dummies

🦥Idempotency for Dummies

Sep 22, 2026

Hello friends!

Welcome to another Sloth Bytes issue. I hope you’re having a good week!

Today we’re finally figuring out what AI means when it says it made your code “idempotent.” It’s way less scary than it sounds, and it might save you from charging someone twice.

Latest Posts

If you’re new, or not yet a subscriber, or just plain missed it, here are some of our recent editions:

🦥 Programmers made a fly play doom

🦥 Decompilation for Dummies

🦥 Tailwind has a sugar daddy now

You're Running Three Databases. You Only Need One.

Events go in a metrics store. Embeddings go in a vector database. Analytics get their own warehouse… Now you're running three systems, three sets of tooling, and pipelines to keep them in sync, all for one app.

TimescaleDB collapses that back into the Postgres you already run. Hypertables handle events at scale. pgvector and pgvectorscale handle embeddings. Continuous aggregates handle real-time analytics. It’s the same SQL, same tools, and only one system to operate. The sync jobs disappear. The drift disappears. The second and third databases disappear.

It's still Postgres, so nothing about your workflow changes except how much you have to maintain. Start on Tiger Cloud and get $1000 in credits.

Get $1000 Credit

Most founders are one system away from turning LinkedIn into their best sales channel.

Engagement is easy to mistake for pipeline. On Sep 30, watch how a founder turns LinkedIn content into real outreach. Live. You'll walk away with a repeatable system: what to post, who to reach out to, and how to sequence it. 

Eligible startups also get the LinkedIn-to-Leads Toolkit: ad credits, Apollo, Captions, and HubSpot's Prospecting Agent.

Claim my spot

Idempotency for Dummies

If you code with AI, you’ve probably seen this:

  • “I made the endpoint idempotent.”

  • “Added idempotency so retries are safe.”

And you probably hit accept and moved on like you knew what that meant.

No judgment. I do the same.

But it’s actually the reason you don’t get charged twice when you accidentally double-click “Pay Now.”

And once you understand it, you’ll start noticing all the places your own code can accidentally do stuff twice and why it’s recommended everywhere.

What Is Idempotency?

It’s a big scary word, but actually a simple idea:

An operation is idempotent if doing it once or doing it 10 times leaves things the same.

  • Idempotent: Think of a light switch on the wall. Push it up and the light turns on. Push it up again? It’s already up. Nothing happens. You can slap that switch upward 50 times and you’ll probably break it, but hey, the light stays on. That’s idempotent.

  • Not idempotent: Now think about the power button on your TV remote. Press it once, the TV turns on. Press it again, the TV turns off. Same button, totally different result depending on how many times you hit it.

That’s the whole idea. Your code should know the difference between another attempt and another action.

Quick clarification because this does confuse people:

  • Idempotent doesn’t mean the code only runs once.

The code can run as many times as it wants and the response can even be different. What matters is that the effect doesn’t pile up. That’s literally how HTTP defines it.

Fun fact: GET, PUT, and DELETE are supposed to be idempotent, but POST isn’t. So if you’re building an API, POST is where duplicate bugs love to hide.

Why Should You Care?

Because in real apps, stuff gets repeated constantly. Usually not on purpose.

  • Users spam-click buttons when the internet is slow. (Don’t lie, you do it too.)

  • The Wi-Fi drops mid-request, so the app tries again when it’s back.

  • Background jobs and queues retry tasks that fail.

  • Webhooks can deliver the same event more than once.

None of these situations are bugs. They’re just edge cases that could happen, and unfortunately, since you’re the smart programmer, you have to think about how to handle them.

These situations can lead to bugs when your code treats every retry like a brand new request. That’s how you end up with double charges, two orders, or a welcome email that shows up three times.

One of the sneakiest bugs is when the request actually worked, but the response got lost. Your app thinks it failed, so it retries, and the retry succeeds too. No errors anywhere. Just a duplicate.

Which means nobody knows it’s happening.

AWS wrote a whole article about this exact problem.

4 Ways to Make Something Idempotent

This usually isn’t hard. Sometimes it’s a one-line change. Sometimes your database or API already does it for you.

There’s also more advanced ways to do this, but we’ll keep it simple since the advance ways build off these strategies.

1. Send the final state, not the change

Say you’re building a shopping cart. There are 2 hoodies in the cart, and the user taps the “+” button since they want to buy another one.

There are two ways your app can tell the server to add another one:

// Not idempotent: tell the server "add one more"
async function increaseQuantity(item) {
  await fetch("/api/cart/items/" + item.id, {
    method: "POST",
    body: JSON.stringify({ add: 1 }), // Every retry = one more hoodie.
  });
}

// Idempotent: figure out the final number, then send THAT
async function increaseQuantity(item) {
  const newQuantity = item.quantity + 1;

  await fetch("/api/cart/items/" + item.id, {
    method: "PUT",
    body: JSON.stringify({ quantity: newQuantity }), // A retry sends the same number.
  });
}

The first one says “add one more.” Which makes sense logically.

But what if the request goes through and the response gets lost because of bad Wi-Fi? The app is going to retry, and now you’re buying 4 hoodies. Great.

The second one does the math first, then sends the answer: “I want 3.” If that request gets retried, it still says 3. Retry it 50 times. Still 3 hoodies. And it works for any quantity, since the number comes from what’s already in the cart.

(Also notice it’s a PUT. What a useful fun fact, right?)

The catch: this doesn’t help if requests show up out of order. An old “quantity: 2” can land after a newer “quantity: 3.” A version check fixes that, but that’s a whole other newsletter.

2. Make duplicates impossible

Someone favorites the same song twice. You don’t want two identical favorites sitting in your database.

In JavaScript, a Set only keeps one copy of each value:

const favorites = new Set();

favorites.add("Off the Wall - Michael Jackson"); // First attempt: adds the song.
favorites.add("Off the Wall - Michael Jackson"); // Retry: nothing new happens.

console.log(favorites.size); // 1. Only one song.

This example is cool for learning, but a Set only lives in memory.

Restart your server and it’s gone.

In a real app, you would put that rule in your database with a unique constraint:

ALTER TABLE favorites
ADD CONSTRAINT one_favorite_per_song UNIQUE (user_id, song_id);

Now even if two requests hit at the exact same time, the database refuses the second copy.

The catch: this stops duplicate rows, not duplicate side effects.

If your code also sends a notification like “Someone favorited your song!” on every attempt, the artist can still get it three times. One favorite. Three notifications. It’s still technically progress though.

3. Only let it happen once

Your game gives every new player a bonus of 100 free coins.

Adding coins isn’t idempotent, so players could spam-click the “Claim bonus” button and get a bunch of coins.

The solution here is basically hiring a bodyguard:

// Simple version
const player = { coins: 0, bonusClaimed: false };

function claimBonus() {
  if (player.bonusClaimed) return; // Already claimed? Stop.

  player.coins += 100;            // Give the reward.
  player.bonusClaimed = true;     // Block another claim.
}

claimBonus(); // Balance: 100.
claimBonus(); // Still 100.

The bonusClaimed flag is the bodyguard. Already claimed? You’re not getting in lil bro.

The catch: on a real backend, two requests can check the flag at the exact same moment, both see false, and both hand out coins. That’s a race condition.

The fix is to do the check and the update in one atomic step so nothing can sneak in between. In SQL, that’s just one UPDATE with the check inside the WHERE:

UPDATE players
SET coins = coins + 100,      -- Give the reward...
    bonus_claimed = true      -- ...and lock the door, in the same step.
WHERE id = 42 -- you would give this id from your code btw.
  AND bonus_claimed = false;  -- Only if they haven't claimed it yet.

If two of these hit at the same time, Postgres (with its default settings) makes the second one wait for the first to finish, then re-check the WHERE. By then bonus_claimed is true, so it updates 0 rows.

  • Using Supabase? It’s Postgres under the hood, so put that query in a database function and call it from your app with an RPC call.

  • Using Convex? It’s even lazier. Every mutation is automatically a transaction, so the “obvious” version is already safe:

export const claimBonus = mutation({
  args: { playerId: v.id("players") },
  handler: async (ctx, args) => {
    const player = await ctx.db.get(args.playerId);
    if (!player || player.bonusClaimed) return; // Already claimed? Stop.

    await ctx.db.patch(args.playerId, {
      coins: player.coins + 100,
      bonusClaimed: true,
    });
  },
});

That’s the same read, check, write as our local example. The difference: if two requests collide, Convex detects the conflict and reruns one of them, and the rerun sees the bonus was already claimed. Your code stays simple. The database does the hard part.

Don’t you love modern tools? I sure do.

4. Give each action an idempotency key

This is the big one. It’s how Stripe lets you safely retry payment requests without charging someone twice.

Here’s the problem it solves: Sometimes the same request should happen twice.

  • Sending the same person two different payments? Totally fine.

  • Sending the same payment twice because of a retry? Not fine.

To the server, both look identical. So how does it tell them apart?

You give each action a name tag. That’s the idempotency key.

  • Retrying the same action? Same key.

  • Doing a genuinely new action? New key.

It’s like a receipt number. If you walk up with receipt #42 asking where your burger is, they don’t make you a second burger. They go “oh yeah, #42, it’s coming.”

Payments aren’t the only place this shows up. Here’s the same idea for sending an email with Resend:

const { error } = await resend.emails.send(
  notification.email,                 // The original email.
  { idempotencyKey: notification.id } // Same action = same key.
);

if (error) throw new Error(error.message);

If you retry with the same key within 24 hours, Resend gives you back the original result instead of sending another email. Reuse a key with different content and it gets rejected.

Here’s a useful resource if you wanted to make your own keys

The #1 way people mess this up: generating a brand new key on every retry.

That’s like printing a new receipt every time you ask about your burger. Now you have 4 burgers and a very confused cashier.

The catch: the service you’re calling has to actually support keys. If you want this on your own API, you have to store the keys and handle two requests arriving at once yourself, which is harder than it sounds. Use an existing tool if it covers you.

Which Method Should You Use?

Here’s a cheat sheet:

Strategy

Use it for

Watch out for

1. Send the final state

Settings, toggles, cart quantities

Old requests overwriting newer ones

2. Unique constraint

Favorites, follows, memberships

Doesn’t stop duplicate emails or other side effects

3. Only let it happen once

One-time rewards, status changes

Check and update must happen together

4. Idempotency key

Payments, emails, orders, background jobs

Only works if the service supports it (and keys expire)

Of course you can also mix them. A signup could use a unique constraint for the account, a guarded update for starter credits, and an idempotency key for the welcome email.

So instead of asking “did I prevent duplicates?” ask:

“What in this feature could happen twice, and what stops each one?”

Check Your Tools Before Building It Yourself

Before you write your own idempotency system at 2am or tell AI to “build this, no mistakes,” check if your tools already do it:

Tool

What to look for

Heads up

Stripe

Idempotency keys on API requests

Read the rules on how long keys last and how errors are handled

Resend

Keys for single and batch email sends

Keys last 24 hours; retry with the original email

Inngest

Event IDs and function-level idempotency

Usually 24 hours; batching and debouncing have exceptions

Trigger.dev

idempotencyKey when triggering tasks

Scope and expiration matter; failed runs clear their keys

AWS Lambda Powertools

A built-in idempotency utility

You configure the key, storage, and expiration yourself

Remember: these tools protect different parts of your app.

So if you or your AI ever says “this tool handles it,” make sure you ask: Handles which part, buddy?

How to test idempotency

Don’t just trust that it works. Try to break it:

  • Spam it. Call the same action twice, including at the exact same time.

  • Crash it halfway. Kill it after part of the work succeeds, then retry.

  • Wait it out. Retry after a server restart or after the key expires.

  • Mess with the data. Reuse a key with different data, then do two genuinely separate actions with identical data.

And one last thing: idempotent doesn’t mean “guaranteed to work.”

A request can fail 10 times in a row and still be idempotent. It just won’t create 10 duplicates while failing.

Some useful AI prompts

If you’re too lazy to read all this.
❝

Read this article: Idempotency

Explain idempotency to me in plain English in under 150 words, like I’m a beginner. Use one everyday example and one coding example in [language I use].

Then list the 4 ways to make something idempotent from the article, with one sentence each on when I’d use it.

If you want to add idempotency inside your projects
❝

Read this article on Idempotency and review [feature or workflow] in my project.

Identify every effect that could repeat, such as database changes, emails, payments, or background jobs. Explain what counts as a retry versus a genuinely new action.

Check the current official documentation for my tools and versions. Prefer existing SDK options, unique constraints, and atomic updates over a custom idempotency system.

Recommend the simplest approach, with its benefits and limits. Check concurrency, restarts, key scope, expiration, and external services separately.

Suggest tests for duplicate calls, partial failures, lost responses, changed request data, and legitimate repeated actions. Don’t change files or run migrations until I approve.

Keep Learning

  • Webhooks: Why the same event can arrive more than once.

  • Race conditions: Why “check first, then update” can still break.

  • AWS’s guide to safe retries: More on request identity and failure cases.

That’s all from me!

Have a great week, be safe, make good choices, and have fun coding.

If I made a mistake or you have any questions, feel free to comment below or reply to the email!

See you all next week.

What'd you think of today's email?

  • 🦥 Amazing! Keep it up
  • 🦥 Good, not great
  • 🦥 It sucked

Login or Subscribe to participate

Want to advertise in Sloth Bytes?

If your company is interested in reaching an audience of developers and programming enthusiasts, you may want to advertise with us here.

Keep Reading

Read all
arrow-right
envelope-simple

Join 50k+ developers and become a better programmer and stay up to date in just 5 minutes.

© 2026 Sloth Bytes.
beehiivPowered by beehiiv