@disckit/antiflood
v1.3.0
Published
Advanced per-user+command+guild rate limiter with burst window, progressive penalty and role whitelist.
Maintainers
Readme
Features
- Sliding window — tracks hits over a rolling time window, not a fixed interval
- Progressive penalty —
ADDITIVEorEXPONENTIALbackoff on repeated abuse - Role whitelist — skip checks entirely for specific roles (owners, mods)
- Per-command rules — different limits for heavy commands like
/gamble - Guild isolation — separate buckets per guild by default
stats()— lifetime counters: hits, blocked, whitelisted, block ratio- Master switch —
disable()/enable()for maintenance - Full TypeScript types included · Zero dependencies · Node.js 18+
Installation
npm install @disckit/antiflood
yarn add @disckit/antiflood
pnpm add @disckit/antifloodTypeScript / ESM
// ESM
import { AntifloodManager, FLOOD_RESULT, PENALTY_MODE } from '@disckit/antiflood';
// CommonJS
const { AntifloodManager, FLOOD_RESULT, isBlocked } = require('@disckit/antiflood');Usage
Basic setup
const { AntifloodManager, isBlocked, formatRetryAfter } = require('@disckit/antiflood');
const antiflood = new AntifloodManager({
globalRule: { windowMs: 5000, maxHits: 3, penaltyMode: 'NONE' },
whitelistRoleIds: ['OWNER_ROLE_ID'],
});
// In your interactionCreate handler:
const check = antiflood.check({
userId: interaction.user.id,
guildId: interaction.guildId,
commandName: interaction.commandName,
memberRoleIds: interaction.member.roles.cache.map(r => r.id),
});
if (isBlocked(check)) {
return interaction.reply({
content: `⛔ Devagar! Tente novamente em ${formatRetryAfter(check.retryAfterMs)}.`,
ephemeral: true,
});
}Per-command rules
const { AntifloodManager, PENALTY_MODE } = require('@disckit/antiflood');
const antiflood = new AntifloodManager({
globalRule: { windowMs: 5000, maxHits: 5, penaltyMode: 'NONE' },
});
// Stricter rule for expensive commands
antiflood.setRule('gamble', {
windowMs: 10_000,
maxHits: 1,
penaltyMode: PENALTY_MODE.ADDITIVE,
penaltyStep: 15_000, // +15s per excess hit
maxPenalty: 120_000, // cap at 2 minutes
});Exponential backoff
antiflood.setRule('api-call', {
windowMs: 3000,
maxHits: 2,
penaltyMode: PENALTY_MODE.EXPONENTIAL, // doubles: 1s → 2s → 4s → 8s...
penaltyStep: 1000,
maxPenalty: 30_000,
});Stats monitoring
const s = antiflood.stats();
// → { hits: 1240, blocked: 37, whitelisted: 5, ratio: 0.03 }
console.log(`${s.blocked} blocked / ${s.hits + s.blocked} total (${(s.ratio * 100).toFixed(1)}%)`);
// Reset counters without clearing flood state
antiflood.resetStats();Manual reset
antiflood.reset({ userId: 'USER_ID', commandName: 'gamble', guildId: 'GUILD_ID' });
antiflood.resetAll();API Reference
new AntifloodManager(options?)
| Option | Type | Description |
|--------|------|-------------|
| globalRule | RuleConfig | Default rule applied to all commands |
| whitelistRoleIds | string[] | Role IDs that always bypass all checks |
| enabled | boolean | Master switch. Default: true |
RuleConfig
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| windowMs | number | 5000 | Sliding window size in ms |
| maxHits | number | 3 | Allowed hits per window |
| penaltyMode | "NONE"\|"ADDITIVE"\|"EXPONENTIAL" | "ADDITIVE" | How penalties grow |
| penaltyStep | number | 5000 | Ms added per excess hit |
| maxPenalty | number | 60000 | Hard cap on penalty duration |
check(params) → CheckResult
| Param | Type | Description |
|-------|------|-------------|
| userId | string | Required |
| guildId | string | Optional. Defaults to "*" (global) |
| commandName | string | Optional. Used to look up per-command rule |
| memberRoleIds | string[] | Optional. Checked against whitelist |
Returns { result, retryAfterMs, hitsInWindow, penaltyUntil, rule }.
stats() → { hits, blocked, whitelisted, ratio }
Lifetime counters since the manager was created (or last resetStats()).
FLOOD_RESULT
"ALLOWED" · "THROTTLED" · "PENALIZED" · "WHITELISTED"
Helpers
isBlocked(result) → boolean — true when THROTTLED or PENALIZED
formatRetryAfter(ms) → string — e.g. "2m 30s"
