Capacity Planning skill for Codex

Know which resource runs out first, and when. Codex turns users, traffic shape and a load-tested per-instance number into a sizing plan with every assumption printed next to the result. It applies Little's law to pools and workers, keeps utilisation where queueing does not blow up latency, sizes for a lost zone, checks connection limits, cache memory and quotas, and names the first bottleneck plus the metric that should trigger the next review.

  • Architecture
  • $capacity-planning
  • 143 lines
  • 3 files
  • ~1,800 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- capacity-planning

Installs to ~/.agents/skills/capacity-planning/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $capacity-planning, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Produces capacity plans and back-of-envelope estimates - peak requests per second from users and traffic shape, instances from load-tested per-instance throughput and target utilisation, Little's law concurrency, connection pools, storage and egress growth, zone-failure headroom - with a standard-library calculator script and explicit assumptions. Use when asked how many servers or pods a launch needs, whether the database will survive 10x traffic, to size a queue worker fleet, estimate storage for a year, or sanity-check numbers in a system design.

Use it when

  • A launch, campaign or growth forecast needs a sizing plan
  • You need to know which tier will hit its limit first
  • A system design needs sanity-checked load and storage numbers
  • Queue workers or connection pools need sizing

Use something else when

How to check the result

Feed today's traffic into the model and confirm predicted instance count and utilisation match production within about 20 percent before trusting the forecast.

The mistake it prevents

Sizing from average traffic and theoretical per-server throughput instead of the busiest hour and a load-tested per-instance number at SLO latency.

~/.agents/skills/capacity-planning/SKILL.md

--- name: capacity-planning description: Produces capacity plans and back-of-envelope estimates - peak requests per second from users and traffic shape, instances from load-tested… license: MIT ---

Done well means every number in the plan traces to a measurement or a labelled assumption, the plan names the first resource that will run out and when, peak load fits with a zone down at a utilisation where latency still meets the SLO, and there is a trigger (metric and threshold) that says when to revisit it.

Before estimating

  1. Read AGENTS.md and the deployment config: instance or pod sizes, replica counts, autoscaling limits, database instance class, connection pool sizes.
  2. Collect current numbers from metrics (ask the user for dashboard access or exports if they are not in the repo): peak rps per endpoint, p50/p99 latency, CPU and memory at peak, DB connections, queue depth, storage growth per month. Use the busiest hour of the busiest day in the last 90 days, not the monthly average.
  3. Get the forecast from the user: launch date, expected users, marketing events, seasonal peaks, growth rate. Mark anything you assume.

Core relationships

RelationshipUse
Average rps = daily requests / 86,400From users and actions to load
Peak rps = average x peak factorTypical peak factors: 2 to 4 for consumer daytime traffic, 5 to 10+ for launches, sales or push notification fan-out
Concurrency L = arrival rate x time in system (Little’s law)In-flight requests, threads, DB connections, worker count
Response time ~ service time / (1 - utilisation) (single-queue approximation)Why 60 to 70% target utilisation: at 80% queueing multiplies latency about 5x, at 90% about 10x
Instances = peak rps / (per-instance rps x target utilisation)Fleet size
Storage = records/day x bytes/record x retention x copiesDisk, backups, replicas
Egress bits/s = rps x response bytes x 8Network, CDN and transfer cost

Rough latency scale for sanity checks: in-memory lookup well under a microsecond, same-AZ network round trip around 0.5 ms, SSD random read around 0.1 ms, cross-region round trip 30 to 150 ms. If an estimate implies a request does 200 sequential cross-AZ calls, the design, not the fleet size, is the problem.

Workflow

- [ ] Current peak load and resource use per tier (web, workers, DB, cache, queue)
- [ ] Forecast peak at the planning horizon (growth, events, peak factor)
- [ ] Per-instance capacity from a load test at SLO latency
- [ ] Size each tier at target utilisation; add zone-loss headroom
- [ ] Check derived limits: DB connections, IOPS, cache memory, partition counts, quotas
- [ ] Storage and egress growth over the horizon
- [ ] Identify the first bottleneck and the date it hits
- [ ] Write the plan with assumptions, triggers and cost

1. Measure per-instance capacity

Theoretical numbers mislead. Load test one instance (or one pod) with a realistic request mix and increase load in steps until p99 exceeds the SLO or errors appear; the last good step is the per-instance capacity. Note which resource saturated first (CPU, memory, DB pool, a downstream). Running load tests against shared or production environments needs user approval; default to a staging copy.

2. Run the numbers

Use the bundled calculator so the assumptions print alongside the results:

python3 scripts/capacity.py --dau 2000000 --requests-per-user 40 --peak-factor 3 \
  --per-instance-rps 450 --latency-ms 80 --zones 3 --survive-zone-loss \
  --monthly-growth 0.05 --months 12 \
  --writes-per-user 5 --bytes-per-write 1200 --retention-days 365 --replication 3 \
  --response-kb 8

It prints peak rps now and at the horizon, instances needed, the zone-spread total, utilisation, in-flight requests (Little’s law), data growth and egress. It exits 2 when peak utilisation exceeds 80%, and 2 with a usage message on invalid input. Use --peak-rps instead of --dau when peak traffic is measured directly.

3. Check derived limits

Fleet size is rarely the first limit. For each tier compute:

  • Database connections: instances x pool size per instance must stay under the database’s max connections minus admin headroom. 30 pods x pool 20 = 600 connections, over many managed Postgres defaults; add a pooler (PgBouncer, RDS Proxy) or shrink pools. Size pools with Little’s law: queries/s x mean query time, plus a small margin.
  • Database throughput: peak write rps and read rps against measured IOPS and CPU; replication lag on read replicas under write peaks.
  • Cache: working set size x overhead (Redis uses roughly 1.5 to 2x raw value size with keys and structures) must fit in memory with eviction headroom.
  • Queues and streams: partitions or shards x per-partition throughput; consumer count cannot usefully exceed partitions in Kafka-style systems.
  • Workers: workers = jobs/s x mean job duration, sized for the backlog drain time you can tolerate after a spike.
  • Cloud quotas and limits: instance quotas per region, load balancer limits, API rate limits of third parties at peak.

4. Headroom and failure

  • Size so peak fits with one zone lost (N+1 zones) at no more than about 80 to 85% utilisation; when healthy, that lands near 50 to 60%.
  • Autoscaling covers gradual change, not step changes: scale-up takes minutes (node provisioning, image pull, warm-up). For known spikes, pre-scale and say so in the plan.
  • Keep minimum replicas high enough that a single pod failure does not overload the rest.

Gotchas

  • Averages hide peaks; daily averages hide hourly peaks; per-second spikes from cron jobs or push fan-out hide inside per-minute metrics.
  • Linear extrapolation fails at contention points: lock contention, a hot partition, single-threaded components (Redis main thread, a single DB writer).
  • Retries during incidents multiply load; include a retry factor for the failure case.
  • Storage estimates often forget indexes (commonly 30 to 100% of table size), WAL or binlog, backups, and replicas.
  • Load test numbers from a cold cache or empty database are optimistic; seed realistic data volumes first.

Verification

  • Recompute one number by hand (for example peak rps) and confirm the script agrees.
  • Compare the model with today: plug in current traffic and check the predicted instance count and utilisation match what production shows within about 20%. If not, fix the per-instance capacity or peak factor before forecasting.
  • After launch or at the next peak, record actual versus predicted and update the inputs.

Output template

## Capacity plan: <system>, horizon <date>
**Assumptions:** DAU 2.0M growing 5%/month; 40 req/user/day; peak factor 3 (measured
2026-09); per-instance 450 rps at p99 < 300 ms (load test 2026-10-20, CPU-bound).

| Tier | Peak now | Peak at horizon | Capacity per unit | Units needed | Planned (zone-safe) | Utilisation healthy |
|---|---|---|---|---|---|---|
| API pods | 2,778 rps | 4,988 rps | 450 rps | 19 | 30 (10/zone) | 37% |
| Postgres | 900 conn | ... | max 1,000 | | add PgBouncer | |

**First bottleneck:** Postgres connections in ~4 months at current growth.
**Actions:** <ordered list with owners>
**Revisit when:** peak rps > 4,000 for a week, or p99 > 250 ms at peak, or 2026-04-01.
**Cost estimate:** <monthly, per tier, method>

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Capacity Planning"
  short_description: "Know which resource runs out first, and when"
  default_prompt: "$capacity-planning We expect 2 million daily users by next summer, how many API pods and how big a database?"
scripts/capacity.py126 lines, 6.0 KB
#!/usr/bin/env python3
"""Back-of-envelope capacity estimate: peak load, instances, concurrency, storage, egress.

Standard library only. Every input is explicit so the assumptions show in the output.

Examples:
  python3 capacity.py --dau 2000000 --requests-per-user 40 --peak-factor 3 \
      --per-instance-rps 450 --latency-ms 80 --zones 3 --survive-zone-loss \
      --monthly-growth 0.05 --months 12 \
      --writes-per-user 5 --bytes-per-write 1200 --retention-days 365 --replication 3 \
      --response-kb 8
  python3 capacity.py --peak-rps 1200 --per-instance-rps 300
"""
from __future__ import annotations

import argparse
import math
import sys

SECONDS_PER_DAY = 86_400


def human_bytes(n: float) -> str:
    for unit in ("B", "KB", "MB", "GB", "TB", "PB"):
        if abs(n) < 1000:
            return f"{n:,.1f} {unit}"
        n /= 1000
    return f"{n:,.1f} EB"


def parse() -> argparse.Namespace:
    p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    load = p.add_argument_group("load (give --peak-rps, or --dau with --requests-per-user)")
    load.add_argument("--peak-rps", type=float, help="measured or forecast peak requests/second today")
    load.add_argument("--dau", type=float, help="daily active users today")
    load.add_argument("--requests-per-user", type=float, help="requests per active user per day")
    load.add_argument("--peak-factor", type=float, default=3.0, help="peak / average ratio (default 3)")
    p.add_argument("--monthly-growth", type=float, default=0.0, help="compound growth per month, e.g. 0.05")
    p.add_argument("--months", type=int, default=0, help="planning horizon in months")
    p.add_argument("--per-instance-rps", type=float, required=True,
                   help="max sustainable rps of one instance at SLO latency (from a load test)")
    p.add_argument("--target-util", type=float, default=0.6, help="planned utilisation at peak (default 0.6)")
    p.add_argument("--latency-ms", type=float, help="mean time in system per request, for Little's law")
    p.add_argument("--zones", type=int, default=1, help="availability zones the fleet spreads across")
    p.add_argument("--survive-zone-loss", action="store_true", help="size so peak fits after losing one zone")
    p.add_argument("--writes-per-user", type=float, help="stored records per active user per day")
    p.add_argument("--bytes-per-write", type=float, help="bytes per stored record incl. indexes/overhead")
    p.add_argument("--retention-days", type=float, default=365)
    p.add_argument("--replication", type=float, default=1.0, help="copies kept (replicas, backups)")
    p.add_argument("--response-kb", type=float, help="mean response size in KB, for egress")
    a = p.parse_args()

    if a.peak_rps is None and (a.dau is None or a.requests_per_user is None):
        p.error("give --peak-rps, or both --dau and --requests-per-user")
    for name in ("per_instance_rps", "peak_factor"):
        if getattr(a, name) <= 0:
            p.error(f"--{name.replace('_', '-')} must be > 0")
    if not 0 < a.target_util <= 1:
        p.error("--target-util must be in (0, 1]")
    if a.zones < 1 or (a.survive_zone_loss and a.zones < 2):
        p.error("--survive-zone-loss needs --zones >= 2")
    if a.monthly_growth < 0 or a.months < 0:
        p.error("growth and months must be >= 0")
    return a


def main() -> int:
    a = parse()
    growth = (1 + a.monthly_growth) ** a.months
    rows: list[tuple[str, str]] = []

    if a.peak_rps is not None:
        peak_now = a.peak_rps
        rows.append(("Peak rps today (given)", f"{peak_now:,.0f}"))
    else:
        avg_now = a.dau * a.requests_per_user / SECONDS_PER_DAY
        peak_now = avg_now * a.peak_factor
        rows.append(("Average rps today", f"{avg_now:,.1f}"))
        rows.append((f"Peak rps today (x{a.peak_factor:g})", f"{peak_now:,.0f}"))

    peak = peak_now * growth
    rows.append((f"Peak rps in {a.months} months (x{growth:.2f})", f"{peak:,.0f}"))

    usable = a.per_instance_rps * a.target_util
    needed = max(1, math.ceil(peak / usable))
    rows.append((f"Usable rps per instance ({a.target_util:.0%} of {a.per_instance_rps:g})", f"{usable:,.0f}"))
    rows.append(("Instances to serve peak", str(needed)))

    total = needed
    if a.zones > 1:
        if a.survive_zone_loss:
            total = math.ceil(needed * a.zones / (a.zones - 1))
        total = math.ceil(total / a.zones) * a.zones  # even spread
        label = f"Instances across {a.zones} zones" + (" (survives 1 zone loss)" if a.survive_zone_loss else "")
        rows.append((label, f"{total} ({total // a.zones} per zone)"))
    rows.append(("Utilisation at peak, all healthy", f"{peak / (total * a.per_instance_rps):.0%}"))

    if a.latency_ms:
        conc = peak * a.latency_ms / 1000
        rows.append(("In-flight requests at peak (Little's law)", f"{conc:,.0f}"))
        rows.append(("In-flight per instance", f"{conc / total:,.1f}"))

    if a.dau and a.writes_per_user and a.bytes_per_write:
        daily = a.dau * growth * a.writes_per_user * a.bytes_per_write
        stored = daily * a.retention_days * a.replication
        rows.append(("New data per day (end of horizon)", human_bytes(daily)))
        rows.append((f"Stored at steady state ({a.retention_days:g} d x{a.replication:g} copies)", human_bytes(stored)))
        rows.append(("Write rps from stored records (avg)", f"{a.dau * growth * a.writes_per_user / SECONDS_PER_DAY:,.1f}"))

    if a.response_kb:
        mbps = peak * a.response_kb * 8 / 1000
        rows.append(("Peak egress", f"{mbps:,.0f} Mbit/s"))

    width = max(len(k) for k, _ in rows)
    print("Capacity estimate (assumptions are the inputs; validate per-instance rps by load test)")
    print("-" * (width + 22))
    for k, v in rows:
        print(f"{k:<{width}}  {v}")
    if peak / (total * a.per_instance_rps) > 0.8:
        print("\nWARNING: utilisation above 80% at peak; queueing delay will dominate latency.")
        return 2
    return 0


if __name__ == "__main__":
    sys.exit(main())

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem