Run Code Only Once


Some things should only happen once, like showing a welcome tour or giving out a daily reward. The obvious way is to check a flag, do the thing, then set the flag. But two tabs can both check the flag before either one sets it, and then both do it. This is called a race condition.

puter.kv.incr() avoids it. It adds 1 on the server and returns the new number in one step, so no matter how many calls happen at once, exactly one of them gets back 1. Add Counters covers incr() for regular counting.

Run Once

Call puter.kv.incr() and only run the action when it returns 1:

if ( await puter.kv.incr('welcomeTourShown') === 1 ) {
    showWelcomeTour();
}

The first call returns 1. Every call after that returns 2, 3 and so on, and skips the tour.

Data in puter.kv is stored per user and per app, so this runs once per user, across all their tabs and devices. To run something once across all users, see Run Once Across All Users.

Why Not Use get() and set()

// Two tabs can both get null here, and both show the tour.
if ( await puter.kv.get('welcomeTourShown') === null ) {
    await puter.kv.set('welcomeTourShown', true);
    showWelcomeTour();
}

With one tab this works fine. With two, both can call puter.kv.get() before either calls puter.kv.set(), so both see null and both show the tour.

Run Once per Day

Put the date in the key. Each day gets a new key that starts again at 1:

const today = new Date().toISOString().slice(0, 10);   // '2026-10-02', in UTC
const key = `dailyReward:${ today }`;

if ( await puter.kv.incr(key) === 1 ) {
    await puter.kv.expire(key, 60 * 60 * 24 * 2);   // delete the key after 2 days
    giveDailyReward();
}

The date in the key is what makes it once a day. The puter.kv.expire() call just cleans up old keys, so if the tab closes before it runs, nothing breaks. For once an hour, use toISOString().slice(0, 13) instead.

Allow It a Few Times

puter.kv.incr() returns how many times the action has been tried, so the same check works for limits other than one. This allows three free exports a day:

const used = await puter.kv.incr(`freeExports:${ today }`);

if ( used <= 3 ) {
    await exportFile();
} else {
    showUpgradePrompt();
}

Calls that get turned down still count, so the number can go past 3. That's fine, since anything over 3 is turned down anyway.

Hand Out Numbers in Order

puter.kv.incr() also works like an auto-increment ID in SQL. Every call gets the next number and no two calls get the same one, which is useful for things like invoice numbers:

const number = await puter.kv.incr('nextInvoiceNumber');

await puter.kv.set(`invoice:${ String(number).padStart(6, '0') }`, invoice);
// invoice:000042

If the puter.kv.set() call fails, that number is skipped, so there can be gaps. The zero padding keeps the keys in number order, as explained in Query a Collection.

Run Once Across All Users

Services that send webhooks, like payment providers, will send an event again if your reply is slow or fails. A worker can make sure each event is only handled once by claiming its ID. It uses me.puter.kv, which is your own storage, so every request shares the same keys (see Build an API with a Worker):

router.post('/webhooks/payments', async ({ request }) => {
    const event = await request.json();
    const key = `handled:${ event.id }`;

    if ( await me.puter.kv.incr(key) > 1 ) {
        return { received: true };   // already handled
    }
    await me.puter.kv.expire(key, 60 * 60 * 24 * 7);

    try {
        await handlePayment(event);
    } catch ( error ) {
        await me.puter.kv.del(key);   // let the next retry try again
        throw error;
    }
    return { received: true };
});

Keep the key around longer than the sender keeps retrying. A week is enough for most services. If handling the event fails, the key is deleted so the next retry runs normally.

Notes

← All recipes