Nearly every bug I have fixed in Nakodo that mattered was a thing happening twice. Two emails to the same creator. Two jobs for the same creator. Two charges, nearly. An operator alert that arrived once a day for nine days.
Over a year the app has grown nine different answers to "only once", and I had never put them side by side. They are not nine implementations of one pattern. They are nine different lengths of promise, and picking the wrong length is the actual bug. Here they are from shortest to longest.
1. Once per request: cache()
export const getUser = cache(async (): Promise<SessionUser | null> => { const supabase = await createSupabase(); const { data } = await supabase.auth.getClaims(); ... }); Eleven functions in the app are wrapped in React's cache(). The promise is "once per request, no matter how many components ask", and it exists because the layout, the page and three actions all want the signed-in user. There is a post on the five functions that decide who may read a row, which are all built on this one.
What it is not: a guard. Two requests do the work twice, which is correct and also the reason none of the next eight can be replaced by it.
2. Once per process: a Set, and it is only a hint
// A request worked: forget today's refusal and the email, so the next outage // is news. Both writes do nothing when there's nothing to forget, so this // runs after any request that works; the set keeps the second and later // requests of one invocation from asking again. const healthy = new Set<NotifiedSource>(); A module-level Set in a serverless function is the weakest promise in the app: it lasts as long as the instance, which might be one request or a hundred, and two instances know nothing about each other.
That is fine, as long as nothing depends on it. In both places this pattern appears, the real guarantee is a WHERE clause in the database and the Set only saves a round trip. The moment you let a Set be the lock, you have written a bug that appears exactly as often as your platform cold-starts.
3. Once per transaction: one line of SQL
export function withLock<T>(key: string, fn: (tx: Tx) => Promise<T>): Promise<T> { return db.transaction(async (tx) => { await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${key}))`); return fn(tx); }); } export const withUserLock = <T>(userId: string, fn: (tx: Tx) => Promise<T>) => withLock(userId, fn); Sixteen call sites, and the keys are the design: four fixed strings (setup_reminders, results_email, daily_digest, and a per-user limit_email:<id>), a user id for anything that spends an allowance, a mailbox prefix for anything that sends, and stripe:<customerId> for anything that writes to a subscription.
This is the right tool when the thing that must happen once is a decision made from a read: count what has been used, then write. The lock turns read-then-write into one unit. The locks have their own post, including the part where hashtext is 32 bits so two different keys can collide and serialise each other for no reason.
4. Once per statement: a conditional upsert
const rows = await db .insert(providerDays) .values({ provider, day, requestsUsed: n }) .onConflictDoUpdate({ target: [providerDays.provider, providerDays.day], set: { requestsUsed: sql`${providerDays.requestsUsed} + ${n}` }, setWhere: limit === null ? usable : sql`${providerDays.requestsUsed} + ${n} <= ${limit} and ${usable}`, }) .returning({ used: providerDays.requestsUsed }); return rows.length > 0 && (limit === null || rows[0].used <= limit); setWhere is the part people miss in Postgres' ON CONFLICT DO UPDATE. The condition is evaluated against the locked existing row, so the increment and the cap check are one statement. An update that fails the condition returns no row, and no row is the refusal.
Four sites use it, and all four are places where a lock would be wrong because there is nothing to decide, only something to reserve. If you can express a limit as arithmetic on one row, you do not need a transaction at all.
5. Once until something resets it: a latch column
const [row] = await db .insert(providerState) .values({ provider: source, blockNotifiedAt: sql`now()` }) .onConflictDoUpdate({ target: providerState.provider, set: { blockNotifiedAt: sql`coalesce(${providerState.blockNotifiedAt}, now())` }, }) .returning({ first: sql<boolean>`${providerState.blockNotifiedAt} = now()` }); return row.first; coalesce(column, now()) writes the time only if the column is null, and the returning expression answers "was that me". So the first caller after the latch is clear gets true, every caller after it gets false, and one UPDATE elsewhere sets the column back to null and arms it again.
The promise here is "once per episode", which is the shape every alert wants and almost no alert gets. We learned that the hard way: a paid source ran out of credit and the code hid it for nine days because the guard was per day rather than per episode, so at every midnight it rearmed and sent nothing new.
6. Once while the work is open: a partial unique index
uniqueIndex("jobs_open_dedupe_uq") .on(t.dedupeKey) .where(sql`${t.status} in ('pending', 'running')`), This is my favourite index in the schema. It says: a key may appear once among unfinished jobs, and any number of times among finished ones. Queue the same work twice and the second insert hits the index and is dropped by onConflictDoNothing; queue it again next week, after the first one finished, and it goes in.
Right next to it is the same question asked for all time:
sql`not exists (select 1 from ${jobs} where ${jobs.dedupeKey} = ${key} and (${jobs.status} in ('pending', 'running') ...))` Two different promises, one column, and choosing between them per job type is most of what makes the queue behave.
7. Once for ever: twelve unique indexes
subscriptions_stripe_customer_uq one subscription row per Stripe customer channel_contacts_value_uq a contact once per creator and kind email_preferences_token_uq an unsubscribe token belongs to one address inbound_emails_resend_id_uq one row per message our mail provider delivers outreach_threads_token_uq a conversation's token outreach_threads_mailbox_uq a conversation's own reply address outreach_threads_campaign_channel_uq one conversation per creator per campaign outreach_emails_resend_uq one row per message we send suppressions_value_uq one refusal per address, by hash support_tickets_reference_uq the reference a customer quotes support_tickets_token_uq the link in a support email Eleven of them, plus the partial one above, and 17 onConflictDoNothing calls that lean on them. The pattern those 17 share is that the insert is the check: no select, no if, no race, just write it and let the index decide whether it was new.
Four of these are tokens, and a unique index on a token is the cheapest possible way to make a URL mean exactly one thing. You can watch the other half of that design live, because an unknown token is not an error:
$ curl -s "https://nakodo.app/o/abcdefghjkmn" | grep -o "That link has expired" That link has expired That is a made-up token, and nakodo.app/o/abcdefghjkmn answers 200 with a page rather than a 404 or a stack trace, because the thing on the other end of a link in an email should never look broken.
8. Once at the other end: provider idempotency keys
The previous seven are all about our database. None of them stop an email from going out twice if the request that sent it is retried after the send but before the write.
{ idempotencyKey: `yes-${thread.id}` } { idempotencyKey: `answer-${reply.id}` } { idempotencyKey: `fee-${reply.id}` } { idempotencyKey: `handoff-${thread.id}` } { idempotencyKey: `${APP_TAG}-customer-${user.id}-${sub.updatedAt.getTime()}` } { idempotencyKey: `place-import:${key}`, idempotencyKeyTTL: "2h" } Seventeen sites, and the rule they follow is that the key is derived from the row the work is about, never from a timestamp or a random value, because a retry has to produce the same key. The Stripe one is the interesting exception: it includes sub.updatedAt.getTime(), so it is stable for one attempt at one state and deliberately changes when the subscription moves on.
The background import's key carries a TTL of two hours, which is the honest admission that "for ever" is not what we want there: the same city may legitimately be imported again next month.
9. Once because it does not matter: ignore the payload
export const WEBHOOK_EVENTS = [ "checkout.session.completed", "customer.subscription.created", "customer.subscription.updated", "customer.subscription.deleted", "customer.subscription.paused", "customer.subscription.resumed", "invoice.paid", "invoice.payment_failed", "subscription_schedule.updated", "subscription_schedule.released", "subscription_schedule.canceled", "customer.deleted", ] as const satisfies Stripe.WebhookEndpointCreateParams.EnabledEvent[]; export async function handleStripeEvent(event: Stripe.Event): Promise<void> { if (event.type === "customer.deleted") return forgetStripeCustomer(event.data.object.id); if (!HANDLED.has(event.type)) return; ... const customer = customerOf(event.data.object); if (customer) await syncStripeCustomer(customer); } Twelve event types, one piece of behaviour: work out which customer the event is about, throw the rest of the event away, and read the current state from the API. The comment above the list says it plainly: "Each one only says which customer changed".
No idempotency table, no processed-event ids, no replay window. Duplicate deliveries, out-of-order deliveries and events we never asked for are all the same thing: one extra read of the truth. Then a lock on stripe:<customerId> means two of them cannot write at once.
This is the only one of the nine with no guard in it, and it is the strongest of the nine, because the question "did this already happen" never comes up. The others all exist where something cannot be made idempotent: an email has left, a charge is a charge, an operator's inbox is not a database.
The thing I would tell myself a year ago
Write down the length of the promise before you pick the mechanism.
"Only once ever" is a unique index. "Only once while the work is outstanding" is a partial unique index. "Only once per episode" is a latch with something that clears it. "Only once per decision made from a read" is a lock. "Only once per count" is a conditional upsert. "Only once at the far end" is a provider key derived from a row. And if the operation can simply be repeated without harm, you do not need any of it, which is the one worth aiming for.
Every duplicate-work bug I have had in this app was a mechanism from one row of that list used for a job from another row. The nine-day silent outage was a per-day guard doing a per-episode job. It was not a missing feature, it was the wrong length of promise.
The plan limits all of this is protecting are on the pricing page, and what we keep and for how long is on the privacy page.

