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
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
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.
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