Gaming Backend Rate Limiting
Gaming backends face unique challenges: action spam, leaderboard manipulation, matchmaking abuse, and reward exploitation. This example shows how to implement rate limiting that maintains fair play while keeping games responsive.
Scenario
- Matchmaking: 10 requests per minute (prevent queue spam)
- Game actions: 60 per second (fast but bounded)
- Chat messages: 5 per 10 seconds (prevent spam)
- Leaderboard submissions: 1 per game (prevent score inflation)
- Reward claims: Strict per-item limits (prevent exploitation)
- Friend requests: 20 per hour (prevent spam)
Implementation
src/middleware/gaming-limits.ts
import { hitlimit } from '@joint-ops/hitlimit'
import { redisStore } from '@joint-ops/hitlimit/stores/redis'
const redis = redisStore({
url: process.env.REDIS_URL,
keyPrefix: 'game:rl:'
})
// Matchmaking - prevent queue spam and manipulation
export const matchmakingLimit = hitlimit({
store: redis,
limit: 10,
window: '1m',
key: (req) => `mm:${ req.player.id }`,
response: () => ({
error: 'MATCHMAKING_COOLDOWN',
message: 'Please wait before searching for another match.'
})
})
// In-game actions - high frequency but bounded
export const gameActionLimit = hitlimit({
store: redis,
limit: 60,
window: '1s',
key: (req) => `action:${ req.player.id }:${ req.body.gameId }`,
response: () => ({
error: 'ACTION_RATE_LIMITED',
message: 'Too many actions. Possible automation detected.'
})
})
// In-game chat - prevent spam
export const gameChatLimit = hitlimit({
store: redis,
limit: 5,
window: '10s',
key: (req) => `chat:${ req.player.id }:${ req.body.gameId }`,
response: (info) => ({
error: 'CHAT_COOLDOWN',
message: 'Slow down! Chat again in a moment.',
cooldown: info.resetIn
})
})
// Leaderboard submission - one per game session
export const leaderboardLimit = hitlimit({
store: redis,
limit: 1,
window: '1h', // Per game session
key: (req) => {
const gameSessionId = req.body.sessionId
return `leaderboard:${ req.player.id }:${ gameSessionId }`
},
response: () => ({
error: 'SCORE_ALREADY_SUBMITTED',
message: 'Score already submitted for this game.'
}),
onStoreError: () => 'deny' // Critical - prevent duplicate submissions
})
// Daily reward claims - once per day
export const dailyRewardLimit = hitlimit({
store: redis,
limit: 1,
window: '24h',
key: (req) => `daily:${ req.player.id }`,
response: () => ({
error: 'REWARD_CLAIMED',
message: 'Daily reward already claimed. Come back tomorrow!'
}),
onStoreError: () => 'deny'
})
// Friend requests - prevent spam
export const friendRequestLimit = hitlimit({
store: redis,
limit: 20,
window: '1h',
key: (req) => `friend:${ req.player.id }`,
response: () => ({
error: 'FRIEND_REQUEST_LIMITED',
message: 'Too many friend requests. Please wait.'
})
})
// Gift sending - prevent exploitation
export const giftLimit = hitlimit({
store: redis,
limit: 5,
window: '24h',
key: (req) => `gift:${ req.player.id }`,
response: () => ({
error: 'GIFT_LIMIT_REACHED',
message: 'Daily gift limit reached.'
}),
onStoreError: () => 'deny'
}) Route Configuration
src/routes/game.ts
import { Router } from 'express'
import { requirePlayer } from '../middleware/auth'
import * as limits from '../middleware/gaming-limits'
const router = Router()
router.use(requirePlayer)
// Matchmaking
router.post('/matchmaking/join', limits.matchmakingLimit, matchController.join)
router.post('/matchmaking/cancel', matchController.cancel)
// In-game (WebSocket alternative for HTTP)
router.post('/games/:id/action', limits.gameActionLimit, gameController.action)
router.post('/games/:id/chat', limits.gameChatLimit, gameController.chat)
// Leaderboards
router.post('/leaderboard/submit', limits.leaderboardLimit, leaderboardController.submit)
router.get('/leaderboard/:type', leaderboardController.get)
// Rewards
router.post('/rewards/daily', limits.dailyRewardLimit, rewardController.claimDaily)
// Social
router.post('/friends/request', limits.friendRequestLimit, friendController.sendRequest)
router.post('/gifts/send', limits.giftLimit, giftController.send)
export default router Anti-Cheat Patterns
Rate Limiting is Not Anti-Cheat
Rate limiting complements but does not replace proper anti-cheat systems. Use it alongside server-side validation, replay analysis, and behavioral detection.
src/middleware/anti-exploit.ts
import { hitlimit } from '@joint-ops/hitlimit'
import { redisStore } from '@joint-ops/hitlimit/stores/redis'
const redis = redisStore({ url: process.env.REDIS_URL })
// Detect impossible action speeds
export const humanActionLimit = hitlimit({
store: redis,
limit: 10,
window: '100ms', // 10 actions per 100ms = impossible for humans
key: (req) => `human:${ req.player.id }`,
response: () => ({
error: 'SUSPICIOUS_ACTIVITY',
message: 'Unusual activity detected.',
// Flag for review but don't reveal detection
_internal: { flag: 'possible_automation' }
})
})
// Prevent rapid-fire purchases (exploit prevention)
export const purchaseLimit = hitlimit({
store: redis,
limit: 1,
window: '1s',
key: (req) => `purchase:${ req.player.id }`,
response: () => ({
error: 'PURCHASE_COOLDOWN',
message: 'Please wait before making another purchase.'
}),
onStoreError: () => 'deny' // Critical for economy
})
// Limit score submissions per time period (catch grinding exploits)
export const scoreSubmissionLimit = hitlimit({
store: redis,
limit: 100,
window: '1h',
key: (req) => `scores:${ req.player.id }`,
response: () => ({
error: 'SCORE_LIMIT',
message: 'Maximum games per hour reached. Take a break!'
})
}) Economy Protection
Virtual economies are vulnerable to duplication exploits and rapid transactions. Use strict rate limiting with fail-closed behavior.
src/middleware/economy-limits.ts
import { hitlimit } from '@joint-ops/hitlimit'
import { redisStore } from '@joint-ops/hitlimit/stores/redis'
const redis = redisStore({ url: process.env.REDIS_URL })
// Currency transactions - strict limits
export const currencyTransactionLimit = hitlimit({
store: redis,
limit: 10,
window: '1m',
key: (req) => `currency:${ req.player.id }`,
response: () => ({
error: 'TRANSACTION_LIMIT',
message: 'Too many transactions. Please wait.'
}),
onStoreError: () => 'deny' // CRITICAL: Always fail closed
})
// Item trades - prevent rapid trading (laundering)
export const tradeLimit = hitlimit({
store: redis,
limit: 5,
window: '1h',
key: (req) => `trade:${ req.player.id }`,
response: () => ({
error: 'TRADE_LIMIT',
message: 'Trade limit reached. Please try again later.'
}),
onStoreError: () => 'deny'
})
// Loot box opening - prevent rapid exploitation
export const lootBoxLimit = hitlimit({
store: redis,
limit: 50,
window: '1h',
key: (req) => `lootbox:${ req.player.id }`,
response: () => ({
error: 'LOOTBOX_LIMIT',
message: 'Hourly limit reached. Pace yourself!'
}),
onStoreError: () => 'deny'
}) Gaming Best Practices
- Always fail closed for economy: Currency, items, and rewards must use
onStoreError: () => 'deny' - Sub-second windows for action detection: Use 100ms windows to catch automation
- Per-game-session keys: Prevent score re-submission by keying on session ID
- Separate limits per action type: Chat, movement, abilities need different thresholds
- Log rate limit hits: They can indicate cheating attempts
- Don't reveal detection: Use generic messages to avoid helping cheaters