Skip to content

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 →