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

@gumyn/keyrotator

v1.0.0

Published

A simple API key rotator with automatic failover on rate limits (429 errors)

Readme

🔑 KeyRotator

A simple, zero-dependency API key rotator with automatic failover on rate limits (429 errors).

Perfect for working with LLM APIs (OpenAI, Anthropic, Gemini, etc.) where you have multiple API keys and want to maximize throughput by automatically switching when one hits its rate limit.

Features

  • 🔄 Automatic rotation on rate limit errors (429)
  • 📈 Exponential backoff for retries
  • 🎯 TypeScript-first with full type definitions
  • 🪶 Zero dependencies - works with any HTTP client or SDK
  • Bun-optimized but works in Node.js too
  • 🎨 Flexible - customize rate limit detection, callbacks, and more

Installation

bun add keyrotator
# or
npm install keyrotator

Quick Start

import { KeyRotator } from "keyrotator";

// Create rotator with comma-separated keys
const rotator = new KeyRotator(process.env.API_KEYS); // "key1,key2,key3"

// Execute with automatic rotation on rate limits
const result = await rotator.execute(async (key) => {
  const response = await fetch("https://api.example.com/data", {
    headers: { Authorization: `Bearer ${key}` },
  });
  return response.json();
});

Environment Variables

Set your keys as a comma-separated string:

# .env
GEMINI_API_KEYS=AIza...abc,AIza...def,AIza...ghi
OPENAI_API_KEYS=sk-...123,sk-...456
import { KeyRotator } from "keyrotator";

// Create from environment variable
const rotator = KeyRotator.fromEnv("GEMINI_API_KEYS");

Usage Examples

With Google Gemini

import { KeyRotator } from "keyrotator";
import { GoogleGenerativeAI } from "@google/generative-ai";

const rotator = new KeyRotator(process.env.GEMINI_API_KEYS!, {
  onRotate: (from, to, total) => {
    console.log(`🔄 Switched from key #${from} to #${to} (${total} total)`);
  },
});

async function generateContent(prompt: string) {
  return rotator.execute(async (key) => {
    const client = new GoogleGenerativeAI(key);
    const model = client.getGenerativeModel({ model: "gemini-pro" });
    const result = await model.generateContent(prompt);
    return result.response.text();
  });
}

With OpenAI

import { KeyRotator } from "keyrotator";
import OpenAI from "openai";

const rotator = new KeyRotator(process.env.OPENAI_API_KEYS!);

async function chat(message: string) {
  return rotator.execute(async (key) => {
    const client = new OpenAI({ apiKey: key });
    const response = await client.chat.completions.create({
      model: "gpt-4",
      messages: [{ role: "user", content: message }],
    });
    return response.choices[0].message.content;
  });
}

With Anthropic

import { KeyRotator } from "keyrotator";
import Anthropic from "@anthropic-ai/sdk";

const rotator = new KeyRotator(process.env.ANTHROPIC_API_KEYS!);

async function complete(prompt: string) {
  return rotator.execute(async (key) => {
    const client = new Anthropic({ apiKey: key });
    const response = await client.messages.create({
      model: "claude-3-opus-20240229",
      max_tokens: 1024,
      messages: [{ role: "user", content: prompt }],
    });
    return response.content[0].text;
  });
}

Using wrap() for Cleaner Code

const rotator = new KeyRotator(process.env.API_KEYS!);

// Create a wrapped function
const fetchWithKey = rotator.wrap(async (key: string, url: string) => {
  const res = await fetch(url, {
    headers: { "X-API-Key": key },
  });
  return res.json();
});

// Use it without thinking about keys
const data = await fetchWithKey("/api/users");
const posts = await fetchWithKey("/api/posts");

API Reference

Constructor

new KeyRotator(keys: string | string[], options?: KeyRotatorOptions)

Parameters

| Parameter | Type | Description | |-----------|------|-------------| | keys | string \| string[] | Comma-separated keys string or array of keys | | options | KeyRotatorOptions | Optional configuration |

KeyRotatorOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | delimiter | string | "," | Delimiter for parsing keys string | | maxRetriesPerKey | number | 3 | Max retries before rotating to next key | | retryDelayMs | number | 1000 | Initial delay between retries (ms) | | backoffMultiplier | number | 1.5 | Multiplier for exponential backoff | | maxDelayMs | number | 30000 | Maximum delay between retries (ms) | | isRateLimitError | function | built-in | Custom rate limit detection | | onRotate | function | - | Callback when key rotation occurs | | onRetry | function | - | Callback on each retry attempt | | onAllKeysExhausted | function | - | Callback when all keys are exhausted |

Methods

execute<T>(fn: (key: string) => Promise<T>, options?: ExecuteOptions): Promise<T>

Execute a function with automatic key rotation on rate limit errors.

const result = await rotator.execute(async (key) => {
  // Use key to make API call
  return apiCall(key);
});

wrap<TArgs, TResult>(fn: (key: string, ...args: TArgs) => Promise<TResult>)

Create a wrapped function that automatically handles key rotation.

const wrappedFn = rotator.wrap(myApiFunction);
const result = await wrappedFn(arg1, arg2);

rotate(): boolean

Manually rotate to the next key. Returns false if wrapped around to first key.

reset(): void

Reset rotation to the first key.

status(): object

Get current status with masked keys for safe logging.

console.log(rotator.status());
// { currentKey: 1, totalKeys: 3, maskedKeys: ["AIza...abc", "AIza...def", "AIza...ghi"] }

Properties

| Property | Type | Description | |----------|------|-------------| | currentKey | string | The current API key | | currentKeyIndex | number | Current key index (1-based) | | totalKeys | number | Total number of keys | | allKeys | readonly string[] | All keys (frozen array) |

Static Methods

KeyRotator.fromEnv(envVar?: string, options?: KeyRotatorOptions): KeyRotator

Create a KeyRotator from an environment variable.

const rotator = KeyRotator.fromEnv("MY_API_KEYS");

Helper Functions

createKeyRotator(envVarOrKeys: string, options?: KeyRotatorOptions): KeyRotator

Convenience function that auto-detects if the input is keys or an env var name.

// From env var
const rotator1 = createKeyRotator("API_KEYS");

// From keys string
const rotator2 = createKeyRotator("key1,key2,key3");

Custom Rate Limit Detection

By default, KeyRotator detects rate limits by:

  • HTTP status code 429
  • "429" in error message
  • "rate limit" in error message (case-insensitive)
  • "quota" in error message (case-insensitive)
  • "too many requests" in error message (case-insensitive)

You can provide custom detection:

const rotator = new KeyRotator(keys, {
  isRateLimitError: (error) => {
    // Custom logic
    if (error instanceof MyCustomError) {
      return error.code === "RATE_LIMITED";
    }
    return false;
  },
});

Callbacks

const rotator = new KeyRotator(keys, {
  onRotate: (fromIndex, toIndex, totalKeys) => {
    console.log(`🔄 Rotated from key #${fromIndex} to #${toIndex}`);
  },

  onRetry: (keyIndex, attempt, error, delayMs) => {
    console.log(`⏳ Retry #${attempt} on key #${keyIndex}, waiting ${delayMs}ms`);
  },

  onAllKeysExhausted: () => {
    console.warn("⚠️ All API keys have hit their rate limits!");
  },
});

Abort Support

const controller = new AbortController();

// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);

try {
  await rotator.execute(fetchData, { signal: controller.signal });
} catch (error) {
  if (error.message === "Operation aborted") {
    console.log("Request was cancelled");
  }
}

Error Handling

import { KeyRotator, KeyRotatorError } from "keyrotator";

try {
  await rotator.execute(apiCall);
} catch (error) {
  if (error instanceof KeyRotatorError) {
    console.error(`All keys exhausted after ${error.attempts} attempts`);
    console.error("Last error:", error.lastError);
  } else {
    // Non-rate-limit error (thrown immediately)
    console.error("API error:", error);
  }
}

Testing

bun test

License

MIT