Rate Limit Planner — Java 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 Java 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: Java (17+, 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
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public final class RateLimitPlanner {
private static final long WINDOW_MS = 60_000L;
private static final double DEFAULT_SAFETY = 0.8;
/** Requests/tokens per minute; null = not limited. */
public record RateLimits(Double rpm, Double tpm) {}
public record Workload(long requests, long avgTokensPerRequest) {}
public record PlanOptions(Double safetyFactor) {}
public record BatchSlice(long batch, long atMs, long requests, long tokens) {}
public 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: "rpm", "tpm", "both", or "none". */
String boundedBy,
/** First batches of the schedule (max 10). */
List<BatchSlice> timeline,
/** Estimated total wall time, in ms (Infinity when impossible). */
double totalMs,
List<String> warnings) {}
/**
* Plan a schedule.
* @throws IllegalArgumentException on impossible inputs (the TS RangeError contract).
*/
public static RateLimitPlan planRateLimit(
RateLimits limits, Workload workload, PlanOptions opts) {
double sf = opts == null || opts.safetyFactor() == null
? DEFAULT_SAFETY : opts.safetyFactor();
List<String> warnings = new ArrayList<>();
if (workload.requests() < 0 || workload.avgTokensPerRequest() < 0) {
throw new IllegalArgumentException("requests and avgTokensPerRequest must be >= 0");
}
if (sf <= 0 || sf > 1) {
throw new IllegalArgumentException("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", List.of(), Double.POSITIVE_INFINITY,
List.of(String.format(Locale.US,
"A single request averages %,d tokens but the effective token " +
"limit is %,.0f/min — no schedule can run this. Shrink requests " +
"or raise the tier.",
workload.avgTokensPerRequest(), Math.floor(tpmEff))));
}
double byRpm = rpmEff != null ? rpmEff : Double.POSITIVE_INFINITY;
double byTokens = tpmEff == null || workload.avgTokensPerRequest() == 0
? Double.POSITIVE_INFINITY
: tpmEff / workload.avgTokensPerRequest();
boolean noRpm = Double.isInfinite(byRpm);
boolean noTokens = Double.isInfinite(byTokens);
if (noRpm && noTokens) {
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 = noRpm && noTokens ? "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 = Math.round((double) WINDOW_MS / 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.ceil(steady / 4.0));
List<BatchSlice> timeline = new ArrayList<>();
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 * WINDOW_MS, take,
take * workload.avgTokensPerRequest()));
remaining -= take;
batch += 1;
}
long windowsNeeded = workload.requests() > 0
? (long) Math.ceil(workload.requests() / (double) steady) : 0;
long lastWindowRequests = windowsNeeded > 0
? workload.requests() - (windowsNeeded - 1) * steady : 0;
double totalMs = windowsNeeded > 0
? (windowsNeeded - 1) * WINDOW_MS + intervalMs * (double) 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);
}
/** Human summary line for the plan. */
public static String describePlan(RateLimitPlan plan) {
if (plan.batchSize() == 0) return "No viable schedule.";
if ("none".equals(plan.boundedBy())) {
return plan.batchSize() + "+ requests per window — endpoint treated as unbounded.";
}
String limiter = "both".equals(plan.boundedBy())
? "both limits bind together"
: "the " + plan.boundedBy().toUpperCase(Locale.ROOT) + " limit binds first";
return plan.batchSize() + " requests per 60s window (one every " + plan.intervalMs()
+ "ms) — " + limiter + ".";
}
private RateLimitPlanner() {}
}
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 →