Skip to main content

Command Palette

Search for a command to run...

API Rate Limiting: Sliding Window Implementation

Published
7 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

API Rate Limiting Algorithms: Sliding Window Implementation

Rate limiting has become non-negotiable in 2026's API landscape. With AI agents generating unprecedented request volumes and distributed systems scaling horizontally, the naive rate limiting approaches of the past simply don't cut it anymore. If you're still using fixed window counters, you're likely experiencing burst traffic exploitation and poor user experience during window boundaries.

The sliding window algorithm offers a sophisticated middle ground—providing the accuracy of sliding logs without the memory overhead, while eliminating the boundary exploitation issues that plague fixed windows. Let's dive into a production-ready implementation.

The Problem with Traditional Rate Limiting

Modern APIs face a unique challenge: legitimate traffic patterns have become increasingly bursty. AI-powered applications, batch processing systems, and microservice architectures generate request patterns that traditional rate limiters handle poorly.

Fixed window counters are simple but fundamentally flawed. Consider a 100 requests-per-minute limit with a window starting at the top of each minute. A client can send 100 requests at 12:00:59, then another 100 at 12:01:00—effectively doubling your intended rate limit within two seconds. This "boundary exploitation" can overwhelm downstream services.

Sliding log algorithms solve this by tracking every request timestamp, but they're memory-intensive. At scale, storing individual timestamps for millions of users becomes prohibitively expensive. A user making 1,000 requests per hour requires storing 1,000 timestamps—multiply that across your user base.

Token bucket and leaky bucket algorithms work well for smoothing traffic but don't provide the intuitive "X requests per Y time period" semantics that developers expect and that align with business requirements.

Why Sliding Window Wins

The sliding window counter algorithm combines the best of both worlds. It approximates a true sliding window using only two counters—the current window and the previous window—making it memory-efficient while maintaining accuracy.

The key insight: at any given moment, you can estimate the request count in the sliding window by combining a weighted portion of the previous window with the current window's count.

Modern TypeScript Implementation

Here's a production-grade sliding window rate limiter built for Redis and TypeScript:

import { Redis } from 'ioredis';

interface RateLimitConfig {
  maxRequests: number;
  windowMs: number;
  keyPrefix?: string;
}

interface RateLimitResult {
  allowed: boolean;
  remaining: number;
  resetAt: Date;
  retryAfter?: number;
}

export class SlidingWindowRateLimiter {
  private redis: Redis;
  private config: Required<RateLimitConfig>;

  constructor(redis: Redis, config: RateLimitConfig) {
    this.redis = redis;
    this.config = {
      keyPrefix: 'ratelimit',
      ...config,
    };
  }

  async checkLimit(identifier: string): Promise<RateLimitResult> {
    const now = Date.now();
    const currentWindow = Math.floor(now / this.config.windowMs);
    const previousWindow = currentWindow - 1;

    const currentKey = `${this.config.keyPrefix}:${identifier}:${currentWindow}`;
    const previousKey = `${this.config.keyPrefix}:${identifier}:${previousWindow}`;

    // Use Redis pipeline for atomic operations
    const pipeline = this.redis.pipeline();
    pipeline.get(previousKey);
    pipeline.incr(currentKey);
    pipeline.expire(currentKey, Math.ceil(this.config.windowMs / 1000) * 2);

    const results = await pipeline.exec();

    if (!results) {
      throw new Error('Redis pipeline execution failed');
    }

    const previousCount = parseInt(results[0][1] as string || '0', 10);
    const currentCount = results[1][1] as number;

    // Calculate the weighted count
    const windowProgress = (now % this.config.windowMs) / this.config.windowMs;
    const previousWeight = 1 - windowProgress;
    const estimatedCount = Math.floor(
      previousCount * previousWeight + currentCount
    );

    const allowed = estimatedCount <= this.config.maxRequests;
    const remaining = Math.max(0, this.config.maxRequests - estimatedCount);

    const resetAt = new Date((currentWindow + 1) * this.config.windowMs);
    const retryAfter = allowed ? undefined : Math.ceil(
      (resetAt.getTime() - now) / 1000
    );

    return {
      allowed,
      remaining,
      resetAt,
      retryAfter,
    };
  }

  async reset(identifier: string): Promise<void> {
    const pattern = `${this.config.keyPrefix}:${identifier}:*`;
    const keys = await this.redis.keys(pattern);

    if (keys.length > 0) {
      await this.redis.del(...keys);
    }
  }
}

Usage example:

const limiter = new SlidingWindowRateLimiter(redis, {
  maxRequests: 100,
  windowMs: 60000, // 1 minute
});

const result = await limiter.checkLimit('user:12345');

if (!result.allowed) {
  throw new TooManyRequestsError(
    `Rate limit exceeded. Retry after ${result.retryAfter}s`
  );
}

Critical Implementation Pitfalls

Clock skew in distributed systems: When running multiple application servers, ensure they're synchronized via NTP. Even small clock differences can cause inconsistent rate limiting behavior. Consider using Redis's TIME command as a single source of truth.

Redis key expiration timing: We set expiration to windowMs * 2 rather than just windowMs because we need the previous window's data. Setting it too short causes false negatives; too long wastes memory.

Race conditions: The pipeline approach ensures atomicity, but be aware that INCR happens before we check the limit. This means you might slightly exceed your limit during high concurrency. For stricter enforcement, use Lua scripts:

local current_key = KEYS[1]
local previous_key = KEYS[2]
local max_requests = tonumber(ARGV[1])
local window_progress = tonumber(ARGV[2])

local previous_count = tonumber(redis.call('GET', previous_key) or '0')
local current_count = tonumber(redis.call('GET', current_key) or '0')

local estimated = math.floor(previous_count * (1 - window_progress) + current_count)

if estimated < max_requests then
  redis.call('INCR', current_key)
  return {1, estimated + 1}
else
  return {0, estimated}
end

Memory considerations: While more efficient than sliding logs, you're still storing two counters per identifier. With millions of users, implement key eviction strategies and monitor Redis memory usage.

Best Practices for Production

Layer your rate limits: Implement multiple tiers—per-user, per-IP, and global limits. This prevents both individual abuse and coordinated attacks.

Provide clear feedback: Return standard X-RateLimit-* headers in your responses:

response.headers.set('X-RateLimit-Limit', config.maxRequests.toString());
response.headers.set('X-RateLimit-Remaining', result.remaining.toString());
response.headers.set('X-RateLimit-Reset', result.resetAt.toISOString());

Implement graceful degradation: When Redis is unavailable, decide whether to fail open (allow all requests) or fail closed (deny all requests) based on your security requirements.

Monitor and alert: Track rate limit hit rates, false positives, and Redis performance. High hit rates might indicate legitimate users being blocked or attackers probing your limits.

Consider distributed rate limiting: For multi-region deployments, evaluate whether you need global rate limiting (more complex, requires cross-region Redis) or per-region limits (simpler, but allows higher total throughput).

Frequently Asked Questions

Q: How accurate is the sliding window approximation? A: The maximum error is bounded by the window size. In the worst case, you might allow up to maxRequests * 2 during a window transition, but this converges quickly. For most applications, this accuracy is acceptable and far better than fixed windows.

Q: Can I use this without Redis? A: Yes, but you'll lose distributed coordination. In-memory implementations work for single-server deployments. Consider using a distributed cache like Memcached or a database with fast atomic operations.

Q: How do I handle different rate limits for different user tiers? A: Pass the limit configuration dynamically based on user tier. Store tier information in your authentication token or fetch it during the rate limit check.

Q: What's the performance impact? A: Each check requires 2-3 Redis operations (GET, INCR, EXPIRE). With Redis pipelining, this typically adds <5ms latency. For ultra-low latency requirements, consider caching rate limit state locally with periodic Redis synchronization.

Q: Should I rate limit before or after authentication? A: Implement both. Use IP-based rate limiting before authentication to prevent brute force attacks, and user-based limiting after authentication for API usage control.

Q: How do I test rate limiting logic? A: Mock time progression in tests. Inject a clock interface and advance it programmatically. Test boundary conditions, concurrent requests, and window transitions explicitly.

Q: Can this handle burst allowances? A: The basic implementation doesn't support burst credits. For burst handling, combine this with a token bucket algorithm or implement a "burst multiplier" that allows exceeding the limit briefly.

Conclusion

The sliding window algorithm represents the pragmatic choice for modern API rate limiting. It provides the accuracy developers need, the efficiency operations teams require, and the fairness users deserve—all without the complexity of more sophisticated approaches.

As API traffic patterns continue evolving with AI agents and distributed systems, your rate limiting strategy must evolve too. The implementation provided here serves as a foundation, but remember to monitor, measure, and adjust based on your specific traffic patterns and business requirements.

Rate limiting isn't just about protecting your infrastructure—it's about ensuring fair resource allocation and maintaining service quality for all users. Implement it thoughtfully, test it thoroughly, and your APIs will be better prepared for whatever 2026 throws at them.


Metadata

```json { "seo_title": "Sliding Window Rate Limiting: TypeScript Implementation Guide", "meta_description": "Learn how to implement sliding window rate limiting for APIs using TypeScript and Redis. Includes production-ready code, pitfalls to avoid, and best practices for 2026.", "primary_keyword": "sliding window rate limiting", "secondary_keywords": [ "API rate limiting algorithms", "TypeScript rate limiter", "Redis rate limiting", "distributed rate limiting", "rate limit implementation", "API throttling", "sliding window counter", "rate limiting best practices" ], "tags": [ "API Development", "Rate Limiting", "TypeScript", "Redis", "Backend Engineering", "System Design", "Performance" ] }