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 →