E-commerce Rate Limiting

E-commerce platforms face unique challenges: cart manipulation, checkout abuse, inventory scraping, and flash sale bots. This example shows how to implement comprehensive rate limiting that protects your business without blocking legitimate customers.

Scenario

  • Add to cart: 30 items per minute (prevent cart stuffing)
  • Checkout initiation: 5 per hour (prevent checkout bombing)
  • Coupon validation: 10 per minute (prevent brute forcing)
  • Inventory check: 60 per minute (prevent stock scraping)
  • Product search: 30 per minute (prevent catalog scraping)
  • Flash sale endpoints: Special burst limits

Implementation

src/middleware/ecommerce-limits.ts
import { hitlimit } from '@joint-ops/hitlimit'
import { redisStore } from '@joint-ops/hitlimit/stores/redis'

const redis = redisStore({ url: process.env.REDIS_URL })

// Cart operations - prevent stuffing and manipulation
export const addToCartLimit = hitlimit({
  store: redis,
  limit: 30,
  window: '1m',
  key: (req) => {
    // Use session ID for guests, user ID for logged in
    return req.user?.id || req.sessionID
  },
  response: () => ({
    error: 'CART_RATE_LIMITED',
    message: 'Too many items added. Please wait a moment.'
  })
})

// Checkout - strict limits to prevent abuse
export const checkoutLimit = hitlimit({
  store: redis,
  limit: 5,
  window: '1h',
  key: (req) => {
    // Combine user/session with IP for extra protection
    const userId = req.user?.id || req.sessionID
    return `checkout:${ userId }:` + req.ip
  },
  response: (info) => ({
    error: 'CHECKOUT_RATE_LIMITED',
    message: 'Too many checkout attempts. Please try again later.',
    retryAfter: info.resetIn
  }),
  onStoreError: () => 'deny' // Fail closed for checkout
})

// Coupon validation - prevent brute forcing
export const couponLimit = hitlimit({
  store: redis,
  limit: 10,
  window: '1m',
  key: (req) => req.user?.id || req.ip,
  response: () => ({
    error: 'COUPON_RATE_LIMITED',
    message: 'Too many coupon attempts. Please slow down.'
  })
})

// Inventory check - prevent stock monitoring bots
export const inventoryLimit = hitlimit({
  store: redis,
  limit: 60,
  window: '1m',
  key: (req) => req.ip,
  response: () => ({
    error: 'INVENTORY_RATE_LIMITED',
    message: 'Too many requests. Please try again shortly.'
  })
})

// Product search - prevent catalog scraping
export const searchLimit = hitlimit({
  store: redis,
  limit: 30,
  window: '1m',
  key: (req) => req.user?.id || req.ip,
  response: () => ({
    error: 'SEARCH_RATE_LIMITED',
    message: 'Too many searches. Please wait a moment.'
  })
})

// Flash sale - allow burst but strict overall limit
export const flashSaleLimit = hitlimit({
  store: redis,
  limit: 3,
  window: '10s',
  key: (req) => {
    // Strict per-user for flash sales
    const userId = req.user?.id
    if (!userId) return 'guest:' + req.ip
    return `flash:${ userId }`
  },
  response: () => ({
    error: 'FLASH_SALE_LIMIT',
    message: 'Please wait before trying again.'
  }),
  onStoreError: () => 'deny'
})

Route Configuration

src/routes/shop.ts
import { Router } from 'express'
import * as limits from '../middleware/ecommerce-limits'

const router = Router()

// Cart operations
router.post('/cart/add', limits.addToCartLimit, cartController.add)
router.post('/cart/update', limits.addToCartLimit, cartController.update)

// Checkout flow
router.post('/checkout/start', limits.checkoutLimit, checkoutController.start)
router.post('/checkout/complete', limits.checkoutLimit, checkoutController.complete)

// Coupons
router.post('/coupon/validate', limits.couponLimit, couponController.validate)

// Product APIs
router.get('/products/search', limits.searchLimit, productController.search)
router.get('/products/:id/stock', limits.inventoryLimit, productController.checkStock)

// Flash sales (special strict limits)
router.post('/flash-sale/:id/buy', limits.flashSaleLimit, flashSaleController.purchase)

export default router

Flash Sale Strategy

Handling High-Demand Events

Flash sales and limited drops require special handling. The key is to allow legitimate users a fair chance while blocking automated purchases.

src/middleware/flash-sale-protection.ts
import { hitlimit } from '@joint-ops/hitlimit'
import { redisStore } from '@joint-ops/hitlimit/stores/redis'

const redis = redisStore({
  url: process.env.REDIS_URL,
  keyPrefix: 'flash:'
})

// Per-sale limits (one purchase per user per sale)
export const createSaleLimit = (saleId: string) => hitlimit({
  store: redis,
  limit: 1,
  window: '24h', // Sale duration
  key: (req) => {
    const userId = req.user?.id
    if (!userId) throw new Error('Login required for flash sales')
    return `sale:${ saleId }:user:${ userId }`
  },
  response: () => ({
    error: 'ALREADY_PURCHASED',
    message: 'You have already purchased from this flash sale.'
  })
})

// Global request rate (prevent DDoS during high-traffic sales)
export const globalFlashLimit = hitlimit({
  store: redis,
  limit: 100,
  window: '1s',
  key: () => 'global:flash', // Single key for all requests
  response: () => ({
    error: 'HIGH_DEMAND',
    message: 'High demand - please refresh and try again.'
  })
})

Bot Protection Patterns

Common attack patterns and how to mitigate them:

Cart Stuffing

Bots add all inventory to carts to deny stock to real users.

Mitigation: Cart item limits + cart expiration + per-session rate limits

Price Scraping

Competitors scrape your product catalog for pricing intelligence.

Mitigation: Search rate limits + require authentication for bulk access

Checkout Bombing

Repeated checkout attempts to find valid coupon codes or test stolen cards.

Mitigation: Strict checkout limits + coupon validation limits + fail closed

Inventory Monitoring

Bots monitor stock levels to snipe restocks.

Mitigation: Rate limit inventory APIs + add random delays

E-commerce Best Practices

  • Use Redis: In-memory stores won't survive restarts - you need persistent rate limiting for checkout protection
  • Fail closed on checkout: If rate limiting is unavailable, reject checkouts rather than allowing unlimited attempts
  • Track by multiple identifiers: Combine user ID, session, and IP to catch sophisticated attackers
  • Shorter windows for flash sales: Use 10s or 30s windows during high-demand events
  • Return retry-after headers: Help legitimate users know when they can try again