# Cooldowns

Limit how often a command runs with the Cooldown() gate. Covers durations, uses per window, who shares a window, wording the refusal, and keeping windows across restarts.

A command that calls a paid API or posts in a channel needs a limit, or one person can run it dozens of times a minute. By hand, that's a map of last-use times in every handler that needs one, plus a reply telling the caller when to try again.

[`Cooldown`](https://docs.seedcord.org/packages/core/latest/cooldown) is that limit as a gate. It allows a set number of uses per window and refuses once they're used up, with a card that shows when the caller can try again.

```ts title="src/handlers/Feed.ts"
import {
    Cooldown,
    Gated,
    SlashHandler,
    SlashRoute
} from '@seedcord/gateway';

@Gated(Cooldown('30m'))
@SlashRoute('feed')
export class Feed extends SlashHandler<'feed'> {
    public async execute(): Promise<void> {
        await this.reply('Fed.');
    }
}
```

## Adding options

Start with a duration alone, then add options as the command needs them. Each line below adds one option to the line above it.

```ts
// one use per caller every 30 seconds
Cooldown(30);

// three uses per caller every ten minutes
Cooldown('10m', { limit: 3 });

// three uses per server every ten minutes
Cooldown('10m', { limit: 3, per: 'guild' });
```

The sections below cover each option in detail.

### The duration

You pass a duration literal as a string, or a number of seconds.

```ts
Cooldown('30m'); // thirty minutes
Cooldown(30); // thirty seconds
```

A duration literal is digits followed by one lowercase unit.

{/* prettier-ignore-start */}

| unit | means        |
| ---- | ------------ |
| `ms` | milliseconds |
| `s`  | seconds      |
| `m`  | minutes      |
| `h`  | hours        |
| `d`  | days         |

{/* prettier-ignore-end */}

> **Warning**
>
> The type accepts any number in front of the unit. The parser accepts whole digits alone. So this compiles, then throws as the module loads.
>
> ```ts
> Cooldown('1.5h');
> ```
>
> That throw stops your bot from starting. The error's code is `GateInvalidCooldownDuration`. Write `Cooldown('90m')` instead.

Everything else the parser rejects throws the same way.

* an uppercase unit, as in `30M`
* a bare number in a string, as in `'30'`
* a zero duration, as in `0s`

### Several uses per window

You set how many uses fit in one window with `limit`. It defaults to `1`.

```ts
Cooldown('10m', { limit: 3 });
```

The window is a *sliding window*, so each use counts for ten minutes after it happens. If someone runs the command at 12:00, 12:02, and 12:05, all three pass. A fourth run at 12:06 is refused. At 12:10 the 12:00 use expires, so the next run after that passes.

### Who shares a window

If you leave `per` out, every caller gets their own window. Pass `'guild'` or `'channel'` to widen it.

```ts
Cooldown('10m', { per: '
```

{/* prettier-ignore-start */}

| `per`                 | one window per | falls back to                                             |
| --------------------- | -------------- | --------------------------------------------------------- |
| `'user'`, the default | caller         | one shared window when the source doesn't carry a user    |
| `'guild'`             | server         | one shared window when the source doesn't carry a server  |
| `'channel'`           | channel        | one shared window when the source doesn't carry a channel |

{/* prettier-ignore-end */}

```ts
Cooldown('1h', { per: 'guild' });
```

With `per: 'guild'`, every caller outside a server shares one global window. If one person uses the command in a DM, they can use up the window for every other DM caller. Pair it with [`GuildOnly`](https://docs.seedcord.org/packages/core/latest/guild-only) when your command should only run in a server anyway.

If two handlers use the same settings, they still get separate windows, since the key includes the route. If one handler has two `Cooldown` calls with different settings, their windows stay separate too.

### Wording the refusal

`message` rewords the refusal. `notice` replaces it with a `Notice` you write. If you set both, seedcord uses `notice`. Both take a function, and seedcord calls it with `resetAt`. That's the time the caller can use the command again, as an `EpochMs` number of milliseconds since 1970.

```ts
import { Cooldown, toEpochSeconds } from '@seedcord/gateway';

Cooldown('10s', {
    per: 'channel',
    message: (resetAt) =>
        `Slow down. Try again <t:${toEpochSeconds(resetAt)}:R>.`
});
```

Discord's `<t:...:R>` markup takes seconds, and [`toEpochSeconds`](https://docs.seedcord.org/packages/utils/latest/to-epoch-seconds) converts it. The default card already prints a relative time.

```ts
Cooldown('1m', { notice: (resetAt) => new SlowDown(resetAt) });
```

## Putting it together

This handler sets every option that works together. Each server gets three feeds every ten minutes. When a server has used all three, the refusal says when the caller can try again.

```ts title="src/handlers/Feed.ts"
import {
    Cooldown,
    Gated,
    SlashHandler,
    SlashRoute,
    toEpochSeconds
} from '@seedcord/gateway';

@Gated(
    Cooldown('10m', {
        limit: 3,
        per: 'guild',
        message: (resetAt) =>
            `Out of feeds. Try again <t:${toEpochSeconds(resetAt)}:R>.`
    })
)
@SlashRoute('feed')
export class Feed extends SlashHandler<'feed'> {
    public async execute(): Promise<void> {
        await this.reply('Fed.');
    }
}
```

It sets `message` and leaves `notice` out, since `notice` would replace the reworded refusal.

## When the slot is charged

`Cooldown` is an effect gate. It reads the window during the check and records the use only after every gate on the handler has passed. If a later gate refuses, the commit never runs, so the caller keeps their use.

> **Note**
>
> If two requests for the same key arrive at the same moment, both can read a free slot before either records its use. A tight burst can go over `limit` that way. Every [effect gate](/checks/effect-gates) has this gap between its check and its commit.
>
> `Cooldown` doesn't guard against it, since bursts that tight are rare. If your command can't allow even one extra use, write your own effect gate. In its `commit`, call [`ctx.core.rateLimiter.charge`](https://docs.seedcord.org/packages/types/latest/irate-limiter#charge), which records a use only when a slot is free. Throw when `result.limited` comes back `true`, and put that gate first so no other commit runs before it.

## Keeping windows across restarts

[`core.rateLimiter`](https://docs.seedcord.org/packages/types/latest/irate-limiter) holds the cooldown windows and counts them in memory by default. A restart clears every cooldown, and on a serverless deploy each running copy keeps its own counts. A durable [`store`](/checks/rate-limiter) keeps the windows through both.
