Skip to content

Rate Limit Planner — JavaScript source

Turn RPM/TPM limits into a concrete request schedule — batch size, spacing, binding limit, and total run time, with a safety factor for retries. 100% client-side.

This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

/**
 * Rate Limit Planner — turn provider rate limits plus a workload into a
 * concrete schedule: batch size, spacing, timeline and wall time.
 *
 * Language: JavaScript (ES2022+, ES module; runs unmodified in Node 18+
 *           and modern browsers)
 * Port of src/lib/rateLimitPlanner.ts (the canonical TypeScript
 * implementation).
 * Tool page: https://dev.cosmolabs.org/tools/rate-limit-planner
 *
 * Deterministic — no time reads.
 */

/** Length of a rate-limit window, in ms. */
const WINDOW_MS = 60_000;
/** Default fraction of the limits to target, leaving headroom for retries. */
const DEFAULT_SAFETY = 0.8;

/**
 * Plan a schedule under the given limits.
 *
 * `limits` keys (both optional): `rpm`, `tpm` — requests / tokens per
 * minute; absent = not limited. `workload`: `{ requests,
 * avgTokensPerRequest }`. `opts.safetyFactor` must be in (0, 1]
 * (default 0.8). Throws RangeError on negative workload numbers or an
 * out-of-range safety factor. With no limits at all the plan is unbounded:
 * batchSize/intervalMs/maxConcurrent become Infinity/0/Infinity, matching
 * the TypeScript original.
 */
export function planRateLimit(limits, workload, opts = {}) {
  const sf = opts.safetyFactor ?? DEFAULT_SAFETY;
  const warnings = [];
  if (workload.requests < 0 || workload.avgTokensPerRequest < 0) {
    throw new RangeError('requests and avgTokensPerRequest must be >= 0');
  }
  if (sf <= 0 || sf > 1) {
    throw new RangeError('safetyFactor must be in (0, 1]');
  }

  const rpmEff = limits.rpm !== undefined ? limits.rpm * sf : undefined;
  const tpmEff = limits.tpm !== undefined ? limits.tpm * sf : undefined;

  // Impossible: one request alone exceeds the token budget.
  if (tpmEff !== undefined && workload.avgTokensPerRequest > tpmEff && workload.requests > 0) {
    return {
      batchSize: 0,
      intervalMs: 0,
      maxConcurrent: 0,
      boundedBy: 'tpm',
      timeline: [],
      totalMs: Infinity,
      warnings: [
        `A single request averages ${workload.avgTokensPerRequest.toLocaleString('en-US')} tokens but the effective token limit is ${Math.floor(tpmEff).toLocaleString('en-US')}/min — no schedule can run this. Shrink requests or raise the tier.`,
      ],
    };
  }

  const byRpm = rpmEff ?? Infinity;
  const byTokens =
    tpmEff === undefined || workload.avgTokensPerRequest === 0
      ? Infinity
      : tpmEff / workload.avgTokensPerRequest;

  if (!Number.isFinite(byRpm) && !Number.isFinite(byTokens)) {
    warnings.push('No limits set — the plan assumes an unbounded endpoint. Add RPM or TPM for a real schedule.');
  }

  const steady = Math.max(1, Math.floor(Math.min(byRpm, byTokens)));
  const boundedBy =
    !Number.isFinite(byRpm) && !Number.isFinite(byTokens)
      ? 'none'
      : Math.floor(byRpm) === Math.floor(byTokens)
        ? 'both'
        : byRpm < byTokens
          ? 'rpm'
          : 'tpm';

  // Even pacing inside the window: batchSize requests spread over 60s.
  const intervalMs = Math.round(WINDOW_MS / steady);
  // With even spacing and a per-request latency near intervalMs, one request
  // is in flight at a time; concurrency >1 only helps sub-interval latencies,
  // so the safe published floor is 1 — batch bursts raise it to batchSize.
  const maxConcurrent = steady === 1 ? 1 : Math.min(steady, Math.ceil(steady / 4));

  const timeline = [];
  let remaining = workload.requests;
  let batch = 0;
  while (remaining > 0 && batch < 10) {
    const take = Math.min(steady, remaining);
    timeline.push({
      batch: batch + 1,
      atMs: batch * WINDOW_MS,
      requests: take,
      tokens: take * workload.avgTokensPerRequest,
    });
    remaining -= take;
    batch += 1;
  }

  const windowsNeeded = workload.requests > 0 ? Math.ceil(workload.requests / steady) : 0;
  const lastWindowRequests = windowsNeeded > 0 ? workload.requests - (windowsNeeded - 1) * steady : 0;
  const totalMs =
    windowsNeeded > 0 ? (windowsNeeded - 1) * WINDOW_MS + intervalMs * lastWindowRequests : 0;

  if (rpmEff !== undefined && workload.requests > 0 && steady > byRpm) {
    warnings.push('Rounded up to at least one request per window — even a single request per minute keeps the schedule honest.');
  }

  return { batchSize: steady, intervalMs, maxConcurrent, boundedBy, timeline, totalMs, warnings };
}

/** Human summary line for the plan (used by the island + docs). */
export function describePlan(plan) {
  if (plan.batchSize === 0) return 'No viable schedule.';
  if (plan.boundedBy === 'none') return `${plan.batchSize}+ requests per window — endpoint treated as unbounded.`;
  const limiter =
    plan.boundedBy === 'both' ? 'both limits bind together' : `the ${plan.boundedBy.toUpperCase()} limit binds first`;
  return `${plan.batchSize} requests per 60s window (one every ${plan.intervalMs}ms) — ${limiter}.`;
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →