npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@thinkeloquent/npm-api-rate-limiter

v1.0.0

Published

Production-ready API rate limiter with queue management and distributed support

Readme

npm-api-rate-limiter

A flexible NodeJS API rate limiter for throttling requests with queuing, dynamic rate checking, and support for distributed systems via Redis.

Key Features:

  • Request Queuing & Throttling: All API calls are funneled through a schedule() method that places them in a queue. The limiter processes this queue in order, ensuring that requests are never lost and are executed as soon as the rate limit allows.

  • Flexible Rate Limit Strategy:

    • Static Mode: Configure a fixed number of requests per time interval (e.g., 80 requests per 60 seconds).
    • Dynamic Mode: Provide a function to fetch the current rate limit status directly from the API. The limiter will use the real-time remaining calls and reset time to make scheduling decisions, making it highly adaptive.
  • Distributed-Ready with Pluggable Storage:

    • In-Memory (Default): Perfect for single-process applications.
    • Redis Store: Use a shared Redis instance to coordinate rate limits across multiple servers or Node.js processes, preventing the cluster as a whole from exceeding the API quota. The design uses an adapter pattern, making it easy to add other storage backends.
  • High-Throughput Concurrency: The limiter maximizes efficiency by dispatching multiple queued requests in parallel, up to the number of calls currently available in the rate limit window.

  • Automatic and Graceful Back-off: When a rate limit is reached, the class automatically calculates the required wait time (based on the API's reset timestamp or the configured interval) and pauses the queue. It resumes processing automatically once the window resets.

import { API_Rate_Limiter } from "./main.mjs";

// Basic rate limiter with static limits
const limiter = new API_Rate_Limiter("api-calls", {
  maxRequests: 60, // 60 requests
  intervalMs: 60000, // per minute
});

// Wrap your API calls with the limiter
async function fetchData(id) {
  const response = await fetch(`https://api.example.com/data/${id}`);
  return response.json();
}

// Schedule the call through the limiter
const result = await limiter.schedule(() => fetchData(123));

Dynamic Rate Limiting

// Use real-time rate limit info from the API
const limiter = new API_Rate_Limiter("github", {
  getRateLimitStatus: async (resource) => {
    const response = await fetch("https://api.github.com/rate_limit", {
      headers: { Authorization: `token ${process.env.GITHUB_TOKEN}` },
    });
    const data = await response.json();
    return data.resources[resource];
  },
});

Redis Support for Distributed Systems

import { createClient } from "redis";

const redisClient = createClient({ url: "redis://localhost:6379" });
await redisClient.connect();

const limiter = new API_Rate_Limiter("shared-api", {
  maxRequests: 100,
  intervalMs: 60000,
  redisClient,
});

Priority Requests

// High priority request
await limiter.schedule(importantApiCall, { priority: 10 });

// Normal priority
await limiter.schedule(regularApiCall);

Monitoring

// Listen to rate limiter events
limiter.on("rate:limited", (info) => {
  console.log(`Rate limited. Waiting ${info.waitMs}ms`);
});

limiter.on("request:completed", (info) => {
  console.log(`Request completed in ${info.duration}ms`);
});

// Get current statistics
const stats = limiter.getStats();
console.log(`Queue size: ${stats.queueSize}`);

Batch Processing Large Datasets

// Process large dataset in rate-limited batches
async function processLargeDataset(items) {
  const limiter = new API_Rate_Limiter("batch-processor", {
    maxRequests: 50,
    intervalMs: 10000, // 50 requests per 10 seconds
  });

  const results = [];

  for (const item of items) {
    const result = await limiter.schedule(async () => {
      // Process each item
      return await processItem(item);
    });
    results.push(result);

    // Log progress
    if (results.length % 100 === 0) {
      console.log(`Processed ${results.length}/${items.length} items`);
    }
  }

  return results;
}

Basic GitHub Rate Limiting

import { API_Rate_Limiter } from "./main.mjs";

// GitHub allows 5000 requests/hour for authenticated users
// We'll use ~80 requests/minute to stay safe
const githubLimiter = new API_Rate_Limiter("github-core", {
  maxRequests: 80,
  intervalMs: 60000,
});

async function fetchGitHubRepo(owner, repo) {
  return githubLimiter.schedule(async () => {
    const response = await fetch(
      `https://api.github.com/repos/${owner}/${repo}`,
      {
        headers: {
          Authorization: `token ${process.env.GITHUB_TOKEN}`,
          Accept: "application/vnd.github.v3+json",
        },
      }
    );

    if (!response.ok) {
      throw new Error(`GitHub API error: ${response.status}`);
    }

    return response.json();
  });
}

Dynamic GitHub Rate Limiting

// Create a function to check GitHub's rate limit status
async function getGitHubRateLimit(resource = "core") {
  const response = await fetch("https://api.github.com/rate_limit", {
    headers: {
      Authorization: `token ${process.env.GITHUB_TOKEN}`,
    },
  });

  const data = await response.json();
  return data.resources[resource];
}

// Create limiter with dynamic status checking
const dynamicGitHubLimiter = new API_Rate_Limiter("github-dynamic", {
  getRateLimitStatus: getGitHubRateLimit,
});

// Fetch multiple repositories
async function analyzeRepositories(repoList) {
  const repoData = await Promise.all(
    repoList.map(({ owner, repo }) =>
      dynamicGitHubLimiter.schedule(() => fetchGitHubRepo(owner, repo))
    )
  );

  return repoData.map((repo) => ({
    name: repo.full_name,
    stars: repo.stargazers_count,
    forks: repo.forks_count,
    language: repo.language,
  }));
}

// Usage
const repos = [
  { owner: "facebook", repo: "react" },
  { owner: "vuejs", repo: "vue" },
  { owner: "angular", repo: "angular" },
];

const analysis = await analyzeRepositories(repos);
console.log("Repository Analysis:", analysis);

GitHub Search API with Different Rate Limits

// GitHub Search API has different rate limits (30 requests/minute)
const searchLimiter = new API_Rate_Limiter("github-search", {
  maxRequests: 30,
  intervalMs: 60000,
});

async function searchRepositories(query, options = {}) {
  return searchLimiter.schedule(async () => {
    const params = new URLSearchParams({
      q: query,
      sort: options.sort || "stars",
      order: options.order || "desc",
      per_page: options.perPage || 30,
    });

    const response = await fetch(
      `https://api.github.com/search/repositories?${params}`,
      {
        headers: {
          Authorization: `token ${process.env.GITHUB_TOKEN}`,
          Accept: "application/vnd.github.v3+json",
        },
      }
    );

    if (!response.ok) {
      throw new Error(`Search failed: ${response.status}`);
    }

    return response.json();
  });
}

// Search for JavaScript frameworks
const jsFrameworks = await searchRepositories("language:javascript framework");
console.log(`Found ${jsFrameworks.total_count} JavaScript frameworks`);

Multi-Server Setup with Redis

import { API_Rate_Limiter } from "./main.mjs";
import { createClient } from "redis";

// Create Redis client
const redisClient = createClient({
  url: process.env.REDIS_URL || "redis://localhost:6379",
  socket: {
    reconnectStrategy: (retries) => Math.min(retries * 50, 1000),
  },
});

// Handle Redis connection
redisClient.on("error", (err) => console.error("Redis error:", err));
redisClient.on("connect", () => console.log("Connected to Redis"));

await redisClient.connect();

// Create distributed rate limiter
const distributedLimiter = new API_Rate_Limiter("shared-api", {
  maxRequests: 1000, // 1000 requests
  intervalMs: 60000, // per minute
  redisClient, // across all servers
  maxQueueSize: 5000, // larger queue for distributed system
});

// API call function
async function callSharedAPI(endpoint, data) {
  return distributedLimiter.schedule(async () => {
    const response = await fetch(`https://api.shared.com${endpoint}`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "API-Key": process.env.SHARED_API_KEY,
      },
      body: JSON.stringify(data),
    });

    return response.json();
  });
}

// This can run on multiple servers simultaneously
// All instances will share the same rate limit

Microservices Rate Limiting

// Different rate limits for different microservices
class MicroserviceRateLimiter {
  constructor(redisClient) {
    this.limiters = new Map();
    this.redisClient = redisClient;
  }

  getLimiter(service, config) {
    if (!this.limiters.has(service)) {
      this.limiters.set(
        service,
        new API_Rate_Limiter(service, {
          ...config,
          redisClient: this.redisClient,
        })
      );
    }
    return this.limiters.get(service);
  }

  async call(service, config, apiCall) {
    const limiter = this.getLimiter(service, config);
    return limiter.schedule(apiCall);
  }
}

// Usage
const msLimiter = new MicroserviceRateLimiter(redisClient);

// User service: 1000 req/min
await msLimiter.call(
  "user-service",
  { maxRequests: 1000, intervalMs: 60000 },
  () => fetchUserData(userId)
);

// Payment service: 100 req/min (more restricted)
await msLimiter.call(
  "payment-service",
  { maxRequests: 100, intervalMs: 60000 },
  () => processPayment(paymentData)
);

Retry Logic with Exponential Backoff

const resilientLimiter = new API_Rate_Limiter("resilient-api", {
  maxRequests: 50,
  intervalMs: 60000,
  maxRetries: 3,
  retryDelayMs: 1000,
});

// Add custom error handling
resilientLimiter.on("error", (error) => {
  console.error("Rate limiter error:", error);
  // Send to monitoring service
  monitoringService.logError("rate-limiter", error);
});

resilientLimiter.on("request:requeued", ({ retries, metadata }) => {
  console.log(`Request requeued. Attempt ${retries}`, metadata);
});

// API call with metadata for tracking
async function resilientAPICall(data) {
  try {
    const result = await resilientLimiter.schedule(
      async () => {
        const response = await fetch("https://api.example.com/process", {
          method: "POST",
          body: JSON.stringify(data),
        });

        if (response.status === 429) {
          // Rate limit hit - will be automatically retried
          throw new Error(
            `Rate limited: ${response.headers.get("Retry-After")}`
          );
        }

        if (!response.ok) {
          throw new Error(`API error: ${response.status}`);
        }

        return response.json();
      },
      {
        metadata: {
          requestId: data.id,
          timestamp: Date.now(),
        },
      }
    );

    return result;
  } catch (error) {
    if (error.name === "RateLimitError") {
      console.error("Max retries exceeded:", error);
      // Handle permanent failure
    }
    throw error;
  }
}

Handling Different Error Types

class APIErrorHandler {
  constructor(limiter) {
    this.limiter = limiter;
    this.errorCounts = new Map();
  }

  async safeAPICall(apiFunc, options = {}) {
    try {
      return await this.limiter.schedule(apiFunc, options);
    } catch (error) {
      // Categorize errors
      if (error.status === 429 || error.name === "RateLimitError") {
        this.incrementErrorCount("rate_limit");
        throw new Error("API rate limit exceeded. Please try again later.");
      }

      if (error.status >= 500) {
        this.incrementErrorCount("server_error");
        throw new Error("Server error. The API is temporarily unavailable.");
      }

      if (error.status === 401) {
        this.incrementErrorCount("auth_error");
        throw new Error(
          "Authentication failed. Please check your credentials."
        );
      }

      if (error.name === "NetworkError" || error.code === "ECONNREFUSED") {
        this.incrementErrorCount("network_error");
        throw new Error("Network error. Please check your connection.");
      }

      // Unknown error
      this.incrementErrorCount("unknown");
      throw error;
    }
  }

  incrementErrorCount(type) {
    this.errorCounts.set(type, (this.errorCounts.get(type) || 0) + 1);
  }

  getErrorStats() {
    return Object.fromEntries(this.errorCounts);
  }
}

Comprehensive Monitoring Setup

class RateLimiterMonitor {
  constructor(limiter, name) {
    this.limiter = limiter;
    this.name = name;
    this.metrics = {
      totalRequests: 0,
      completedRequests: 0,
      failedRequests: 0,
      rateLimitHits: 0,
      queueHighWaterMark: 0,
      totalWaitTime: 0,
    };

    this.setupListeners();
  }

  setupListeners() {
    this.limiter.on("request:queued", ({ queueSize }) => {
      this.metrics.totalRequests++;
      this.metrics.queueHighWaterMark = Math.max(
        this.metrics.queueHighWaterMark,
        queueSize
      );
    });

    this.limiter.on("request:completed", ({ duration }) => {
      this.metrics.completedRequests++;
      // Track performance metrics
      this.recordDuration(duration);
    });

    this.limiter.on("rate:limited", ({ waitMs }) => {
      this.metrics.rateLimitHits++;
      this.metrics.totalWaitTime += waitMs;
    });

    this.limiter.on("error", (error) => {
      this.metrics.failedRequests++;
      console.error(`[${this.name}] Error:`, error.message);
    });
  }

  recordDuration(duration) {
    // Could send to StatsD, Prometheus, etc.
    console.log(`[${this.name}] Request completed in ${duration}ms`);
  }

  getReport() {
    const stats = this.limiter.getStats();
    return {
      ...this.metrics,
      currentQueueSize: stats.queueSize,
      oldestRequestAge: stats.oldestRequest
        ? Date.now() - stats.oldestRequest
        : 0,
      successRate:
        this.metrics.totalRequests > 0
          ? (
              (this.metrics.completedRequests / this.metrics.totalRequests) *
              100
            ).toFixed(2) + "%"
          : "N/A",
      averageWaitTime:
        this.metrics.rateLimitHits > 0
          ? Math.round(this.metrics.totalWaitTime / this.metrics.rateLimitHits)
          : 0,
    };
  }
}

// Usage
const limiter = new API_Rate_Limiter("monitored-api", {
  maxRequests: 100,
  intervalMs: 60000,
});

const monitor = new RateLimiterMonitor(limiter, "MainAPI");

// Periodically log metrics
setInterval(() => {
  console.log("Rate Limiter Metrics:", monitor.getReport());
}, 60000);

Real-time Dashboard Data

// Export metrics for Prometheus
class PrometheusExporter {
  constructor(limiters) {
    this.limiters = limiters;
  }

  getMetrics() {
    const metrics = [];

    for (const [name, limiter] of this.limiters) {
      const stats = limiter.getStats();

      metrics.push(
        `# HELP api_rate_limiter_queue_size Current queue size`,
        `# TYPE api_rate_limiter_queue_size gauge`,
        `api_rate_limiter_queue_size{resource="${name}"} ${stats.queueSize}`,
        "",
        `# HELP api_rate_limiter_processing Processing status`,
        `# TYPE api_rate_limiter_processing gauge`,
        `api_rate_limiter_processing{resource="${name}"} ${stats.processing ? 1 : 0}`,
        ""
      );
    }

    return metrics.join("\n");
  }
}

// Express endpoint for Prometheus
app.get("/metrics", (req, res) => {
  const exporter = new PrometheusExporter(rateLimiters);
  res.set("Content-Type", "text/plain");
  res.send(exporter.getMetrics());
});

Circuit Breaker Integration

class CircuitBreakerRateLimiter {
  constructor(name, options) {
    this.limiter = new API_Rate_Limiter(name, options);
    this.failures = 0;
    this.successCount = 0;
    this.state = "CLOSED"; // CLOSED, OPEN, HALF_OPEN
    this.nextAttempt = 0;
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 60000;
  }

  async call(apiFunc, options) {
    if (this.state === "OPEN") {
      if (Date.now() < this.nextAttempt) {
        throw new Error("Circuit breaker is OPEN");
      }
      this.state = "HALF_OPEN";
    }

    try {
      const result = await this.limiter.schedule(apiFunc, options);
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failures = 0;

    if (this.state === "HALF_OPEN") {
      this.successCount++;
      if (this.successCount > 3) {
        this.state = "CLOSED";
        this.successCount = 0;
      }
    }
  }

  onFailure() {
    this.failures++;
    this.successCount = 0;

    if (this.failures >= this.failureThreshold) {
      this.state = "OPEN";
      this.nextAttempt = Date.now() + this.resetTimeout;
      console.log(
        `Circuit breaker opened. Will retry at ${new Date(this.nextAttempt)}`
      );
    }
  }

  getState() {
    return {
      state: this.state,
      failures: this.failures,
      nextAttempt: this.state === "OPEN" ? new Date(this.nextAttempt) : null,
    };
  }
}

Priority Queue with Deadlines

class DeadlineAwareRateLimiter extends API_Rate_Limiter {
  constructor(name, options) {
    super(name, options);

    // Check for expired requests periodically
    this.deadlineCheckInterval = setInterval(() => {
      this.removeExpiredRequests();
    }, 5000);
  }

  schedule(apiCall, options = {}) {
    const deadline = options.deadline || Date.now() + 300000; // 5 min default

    return super.schedule(apiCall, {
      ...options,
      metadata: {
        ...options.metadata,
        deadline,
      },
    });
  }

  removeExpiredRequests() {
    const now = Date.now();
    let removed = 0;

    // Filter out expired requests from queue
    const validRequests = [];
    const allRequests = this.queue.dequeue(this.queue.size());

    for (const request of allRequests) {
      if (request.metadata?.deadline && request.metadata.deadline < now) {
        request.reject(new Error("Request deadline exceeded"));
        removed++;
      } else {
        validRequests.push(request);
      }
    }

    // Re-queue valid requests
    validRequests.forEach((req) => this.queue.enqueue(req));

    if (removed > 0) {
      this.emit("requests:expired", { count: removed });
    }
  }

  destroy() {
    clearInterval(this.deadlineCheckInterval);
  }
}

// Usage with deadlines
const deadlineLimiter = new DeadlineAwareRateLimiter("deadline-api", {
  maxRequests: 10,
  intervalMs: 60000,
});

// Critical request with short deadline
try {
  const result = await deadlineLimiter.schedule(() => fetchCriticalData(), {
    priority: 10,
    deadline: Date.now() + 30000, // Must complete within 30 seconds
  });
} catch (error) {
  if (error.message === "Request deadline exceeded") {
    console.error("Request took too long and was cancelled");
  }
}

Multi-Tenant Rate Limiting

class MultiTenantRateLimiter {
  constructor(defaultConfig, redisClient) {
    this.defaultConfig = defaultConfig;
    this.redisClient = redisClient;
    this.tenantLimiters = new Map();
    this.tenantConfigs = new Map();
  }

  // Set custom limits for specific tenants
  setTenantConfig(tenantId, config) {
    this.tenantConfigs.set(tenantId, config);
    // Clear existing limiter to force recreation with new config
    if (this.tenantLimiters.has(tenantId)) {
      this.tenantLimiters.delete(tenantId);
    }
  }

  getLimiterForTenant(tenantId) {
    if (!this.tenantLimiters.has(tenantId)) {
      const config = this.tenantConfigs.get(tenantId) || this.defaultConfig;

      const limiter = new API_Rate_Limiter(`tenant-${tenantId}`, {
        ...config,
        redisClient: this.redisClient,
      });

      this.tenantLimiters.set(tenantId, limiter);
    }

    return this.tenantLimiters.get(tenantId);
  }

  async schedule(tenantId, apiCall, options) {
    const limiter = this.getLimiterForTenant(tenantId);
    return limiter.schedule(apiCall, options);
  }

  getStats() {
    const stats = {};

    for (const [tenantId, limiter] of this.tenantLimiters) {
      stats[tenantId] = limiter.getStats();
    }

    return stats;
  }
}

// Usage
const multiTenantLimiter = new MultiTenantRateLimiter(
  { maxRequests: 100, intervalMs: 60000 }, // Default limits
  redisClient
);

// Premium tenant gets higher limits
multiTenantLimiter.setTenantConfig("premium-tenant-123", {
  maxRequests: 1000,
  intervalMs: 60000,
});

// Regular tenant uses default
await multiTenantLimiter.schedule("regular-tenant-456", () => callAPI("/data"));

// Premium tenant can make more requests
await multiTenantLimiter.schedule(
  "premium-tenant-123",
  () => callAPI("/bulk-data"),
  { priority: 5 }
);