Skip to content

Rate Limit Planner — C# 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 C# 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, what caps it, and finish time.
// Deterministic — no clock reads.
//
// Language: C# (.NET 8, zero dependencies)
// Port of src/lib/rateLimitPlanner.ts (the canonical TypeScript
// implementation). Field names stay camelCase to match the TS surface.
// Tool page: https://dev.cosmolabs.org/tools/rate-limit-planner

using System;
using System.Collections.Generic;
using System.Linq;

namespace CosmoDev.RateLimitPlanner;

public sealed record RateLimits(double? Rpm = null, double? Tpm = null);

public sealed record Workload(long Requests, long AvgTokensPerRequest);

public sealed record PlanOptions(double? SafetyFactor = null);

public sealed record BatchSlice(
    long Batch,
    long AtMs,
    long Requests,
    long Tokens);

public sealed record RateLimitPlan(
    /// Requests to send per 60s window (0 when the workload cannot run).
    long BatchSize,
    /// Steady-state spacing between individual requests, in ms.
    long IntervalMs,
    /// Sustainable concurrent in-flight requests under even spacing.
    long MaxConcurrent,
    /// Which limit binds first.
    string BoundedBy,
    /// First batches of the schedule (max 10).
    IReadOnlyList<BatchSlice> Timeline,
    /// Estimated total wall time, in ms (double.PositiveInfinity when impossible).
    double TotalMs,
    IReadOnlyList<string> Warnings);

public static class RateLimitPlanner
{
    private const long WindowMs = 60_000;
    private const double DefaultSafety = 0.8;

    /// <summary>
    /// Plan a schedule. Throws <see cref="ArgumentOutOfRangeException"/> on
    /// impossible inputs (the TS RangeError contract).
    /// </summary>
    public static RateLimitPlan PlanRateLimit(
        RateLimits limits, Workload workload, PlanOptions? opts = null)
    {
        var sf = opts?.SafetyFactor ?? DefaultSafety;
        var warnings = new List<string>();
        if (workload.Requests < 0 || workload.AvgTokensPerRequest < 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(workload), "requests and avgTokensPerRequest must be >= 0");
        }
        if (sf <= 0 || sf > 1)
        {
            throw new ArgumentOutOfRangeException(
                nameof(opts), "safetyFactor must be in (0, 1]");
        }

        double? rpmEff = limits.Rpm != null ? limits.Rpm * sf : null;
        double? tpmEff = limits.Tpm != null ? limits.Tpm * sf : null;

        // Impossible: one request alone exceeds the token budget.
        if (tpmEff != null && workload.AvgTokensPerRequest > tpmEff && workload.Requests > 0)
        {
            return new RateLimitPlan(0, 0, 0, "tpm", Array.Empty<BatchSlice>(),
                double.PositiveInfinity,
                new[]
                {
                    $"A single request averages {workload.AvgTokensPerRequest:N0} tokens but " +
                    $"the effective token limit is {Math.Floor(tpmEff.Value):N0}/min — no " +
                    "schedule can run this. Shrink requests or raise the tier.",
                });
        }

        double byRpm = rpmEff ?? double.PositiveInfinity;
        double byTokens = tpmEff == null || workload.AvgTokensPerRequest == 0
            ? double.PositiveInfinity
            : tpmEff.Value / workload.AvgTokensPerRequest;

        if (double.IsPositiveInfinity(byRpm) && double.IsPositiveInfinity(byTokens))
        {
            warnings.Add("No limits set — the plan assumes an unbounded endpoint. " +
                "Add RPM or TPM for a real schedule.");
        }

        long steady = Math.Max(1, (long)Math.Floor(Math.Min(byRpm, byTokens)));
        string boundedBy =
            double.IsPositiveInfinity(byRpm) && double.IsPositiveInfinity(byTokens) ? "none"
            : (long)Math.Floor(byRpm) == (long)Math.Floor(byTokens) ? "both"
            : byRpm < byTokens ? "rpm"
            : "tpm";

        // Even pacing inside the window: batchSize requests spread over 60s.
        long intervalMs = (long)Math.Round(WindowMs / (double)steady);
        // Concurrency >1 only helps sub-interval latencies; the safe published
        // floor is 1 — batch bursts raise it to batchSize/4.
        long maxConcurrent = steady == 1 ? 1 : Math.Min(steady, (long)Math.Ceiling(steady / 4.0));

        var timeline = new List<BatchSlice>();
        long remaining = workload.Requests;
        long batch = 0;
        while (remaining > 0 && batch < 10)
        {
            long take = Math.Min(steady, remaining);
            timeline.Add(new BatchSlice(batch + 1, batch * WindowMs, take,
                take * workload.AvgTokensPerRequest));
            remaining -= take;
            batch += 1;
        }

        long windowsNeeded = workload.Requests > 0
            ? (long)Math.Ceiling(workload.Requests / (double)steady) : 0;
        long lastWindowRequests = windowsNeeded > 0
            ? workload.Requests - (windowsNeeded - 1) * steady : 0;
        double totalMs = windowsNeeded > 0
            ? (windowsNeeded - 1) * WindowMs + intervalMs * lastWindowRequests : 0;

        if (rpmEff != null && workload.Requests > 0 && steady > byRpm)
        {
            warnings.Add("Rounded up to at least one request per window — even a single " +
                "request per minute keeps the schedule honest.");
        }

        return new RateLimitPlan(steady, intervalMs, maxConcurrent, boundedBy, timeline,
            totalMs, warnings);
    }

    /// <summary>Human summary line for the plan.</summary>
    public static string DescribePlan(RateLimitPlan plan)
    {
        if (plan.BatchSize == 0) return "No viable schedule.";
        if (plan.BoundedBy == "none")
        {
            return $"{plan.BatchSize}+ requests per window — endpoint treated as unbounded.";
        }
        string limiter = plan.BoundedBy == "both"
            ? "both limits bind together"
            : $"the {plan.BoundedBy.ToUpperInvariant()} 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 →