Skip to content
📚 4 min read

Throttling Examples ​

Learn how to implement throttling patterns for rate limiting and performance optimization.

Basic Usage ​

typescript
// Simple throttle function
function throttle<T extends (...args: any[]) => void>(
  func: T,
  limit: number
): (...args: Parameters<T>) => void {
  let inThrottle = false;

  return function (...args: Parameters<T>) {
    if (!inThrottle) {
      func(...args);
      inThrottle = true;
      setTimeout(() => (inThrottle = false), limit);
    }
  };
}

// Throttle with trailing call
function throttleWithTrailing<T extends (...args: any[]) => void>(
  func: T,
  limit: number
): (...args: Parameters<T>) => void {
  let lastArgs: Parameters<T> | null = null;
  let timeoutId: NodeJS.Timeout | null = null;

  return function (...args: Parameters<T>) {
    if (timeoutId === null) {
      func(...args);

      timeoutId = setTimeout(() => {
        timeoutId = null;
        if (lastArgs) {
          func(...lastArgs);
          lastArgs = null;
        }
      }, limit);
    } else {
      lastArgs = args;
    }
  };
}

Advanced Patterns ​

Promise-based Throttle ​

typescript
interface ThrottledFunction<T extends (...args: any[]) => any> {
  (...args: Parameters<T>): Promise<ReturnType<T>>;
  cancel: () => void;
  flush: () => Promise<ReturnType<T> | undefined>;
}

function throttlePromise<T extends (...args: any[]) => any>(
  func: T,
  limit: number
): ThrottledFunction<T> {
  let timeoutId: NodeJS.Timeout | null = null;
  let lastArgs: Parameters<T> | null = null;
  let lastResolve: ((value: ReturnType<T>) => void) | null = null;
  let lastReject: ((reason: any) => void) | null = null;

  const throttled = (...args: Parameters<T>): Promise<ReturnType<T>> => {
    if (timeoutId === null) {
      return executeFunction(args);
    }

    return new Promise((resolve, reject) => {
      lastArgs = args;
      lastResolve = resolve;
      lastReject = reject;
    });
  };

  const executeFunction = async (
    args: Parameters<T>
  ): Promise<ReturnType<T>> => {
    try {
      const result = await func(...args);

      timeoutId = setTimeout(() => {
        timeoutId = null;
        if (lastArgs) {
          const args = lastArgs;
          const resolve = lastResolve!;
          const reject = lastReject!;

          lastArgs = null;
          lastResolve = null;
          lastReject = null;

          executeFunction(args).then(resolve).catch(reject);
        }
      }, limit);

      return result;
    } catch (error) {
      timeoutId = null;
      throw error;
    }
  };

  throttled.cancel = () => {
    if (timeoutId) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }
    if (lastReject) {
      lastReject(new Error('Throttled function cancelled'));
      lastArgs = null;
      lastResolve = null;
      lastReject = null;
    }
  };

  throttled.flush = async () => {
    if (lastArgs) {
      const args = lastArgs;
      lastArgs = null;
      return func(...args);
    }
    return undefined;
  };

  return throttled;
}

Rate Limiter ​

typescript
interface RateLimiterOptions {
  maxRequests: number;
  interval: number;
  strategy: 'sliding' | 'fixed';
}

class RateLimiter {
  private timestamps: number[] = [];
  private timer: NodeJS.Timeout | null = null;

  constructor(private options: RateLimiterOptions) {
    if (options.strategy === 'fixed') {
      this.resetPeriodically();
    }
  }

  async acquire(): Promise<void> {
    if (this.options.strategy === 'sliding') {
      await this.acquireSlidingWindow();
    } else {
      await this.acquireFixedWindow();
    }
  }

  private async acquireSlidingWindow(): Promise<void> {
    const now = Date.now();
    const windowStart = now - this.options.interval;

    this.timestamps = this.timestamps.filter((t) => t > windowStart);

    if (this.timestamps.length >= this.options.maxRequests) {
      const oldestTimestamp = this.timestamps[0];
      const waitTime = oldestTimestamp + this.options.interval - now;
      await new Promise((resolve) => setTimeout(resolve, waitTime));
      return this.acquireSlidingWindow();
    }

    this.timestamps.push(now);
  }

  private async acquireFixedWindow(): Promise<void> {
    if (this.timestamps.length >= this.options.maxRequests) {
      await new Promise((resolve) => {
        const checkInterval = setInterval(() => {
          if (this.timestamps.length < this.options.maxRequests) {
            clearInterval(checkInterval);
            resolve(undefined);
          }
        }, 100);
      });
    }

    this.timestamps.push(Date.now());
  }

  private resetPeriodically(): void {
    this.timer = setInterval(() => {
      this.timestamps = [];
    }, this.options.interval);
  }

  destroy(): void {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }

  getAvailableTokens(): number {
    return this.options.maxRequests - this.timestamps.length;
  }
}

Adaptive Throttle ​

typescript
interface AdaptiveThrottleOptions {
  initialLimit: number;
  minLimit: number;
  maxLimit: number;
  adaptationFactor: number;
  targetLoad: number;
  measurementWindow: number;
}

class AdaptiveThrottle {
  private currentLimit: number;
  private executionTimes: number[] = [];
  private lastExecutionStart: number = 0;
  private timeoutId: NodeJS.Timeout | null = null;

  constructor(private options: AdaptiveThrottleOptions) {
    this.currentLimit = options.initialLimit;
  }

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.timeoutId) {
      await new Promise((resolve) => {
        const checkInterval = setInterval(() => {
          if (!this.timeoutId) {
            clearInterval(checkInterval);
            resolve(undefined);
          }
        }, 10);
      });
    }

    this.lastExecutionStart = Date.now();

    try {
      const result = await operation();
      this.recordExecution();
      return result;
    } catch (error) {
      this.increaseLimit();
      throw error;
    } finally {
      this.timeoutId = setTimeout(() => {
        this.timeoutId = null;
      }, this.currentLimit);
    }
  }

  private recordExecution(): void {
    const executionTime = Date.now() - this.lastExecutionStart;
    this.executionTimes.push(executionTime);

    if (this.executionTimes.length > this.options.measurementWindow) {
      this.executionTimes.shift();
    }

    this.adjustLimit();
  }

  private adjustLimit(): void {
    if (this.executionTimes.length < this.options.measurementWindow) {
      return;
    }

    const avgExecutionTime =
      this.executionTimes.reduce((a, b) => a + b) / this.executionTimes.length;
    const load = avgExecutionTime / this.currentLimit;

    if (load > this.options.targetLoad) {
      this.increaseLimit();
    } else if (load < this.options.targetLoad * 0.8) {
      this.decreaseLimit();
    }
  }

  private increaseLimit(): void {
    this.currentLimit = Math.min(
      this.options.maxLimit,
      this.currentLimit * this.options.adaptationFactor
    );
  }

  private decreaseLimit(): void {
    this.currentLimit = Math.max(
      this.options.minLimit,
      this.currentLimit / this.options.adaptationFactor
    );
  }

  getCurrentLimit(): number {
    return this.currentLimit;
  }

  getAverageExecutionTime(): number {
    if (this.executionTimes.length === 0) return 0;
    return (
      this.executionTimes.reduce((a, b) => a + b) / this.executionTimes.length
    );
  }
}