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