Skip to main content

Capacity and Sizing

Per-student resource usage

Each active student runs one workspace container. Approximate resource consumption per active student:

ResourceSteady-statePeak (during build)
RAM~1.0–1.5 GB~1.5–2.5 GB
CPUminimal (idle)1–2 cores
Disk (project files)~50 MB~50 MB
Disk (Gradle/editor caches)~500 MB~500 MB

Reserve roughly 4 GB for the OS, Docker daemon, and browser overhead on the host itself.

Container memory cap

Each container is hard-limited to CODE_MEMORY_LIMIT (default: 4096m). If a container exceeds its limit, the Linux OOM killer terminates a process inside it. The student typically sees a failed run; the container itself keeps running.

Be careful lowering this: a cold Gradle build plus the Java language server and codium-server together approach 4 GB. A container pinned at its limit does not always OOM — when most of its memory is reclaimable page cache, the kernel evicts and re-reads the container's files (jars, class files) in a loop instead. That thrashing generates sustained disk reads that can saturate the host's disk throughput and stall the whole VM. CODE_DISK_READ_LIMIT (below) bounds the blast radius, but the right fix is a limit the workload actually fits in.

Adjust in your .env:

# Tighter cap for a memory-constrained host (expect slower cold builds and
# occasional language-server OOMs)
CODE_MEMORY_LIMIT=3072m

Changing CODE_MEMORY_LIMIT requires a control-plane restart to take effect for newly started containers. Already-running containers are not affected until they are next restarted.

Container disk read cap

Each workspace container's disk reads are throttled to CODE_DISK_READ_LIMIT per physical block device (default: 64mb, via Docker --device-read-bps). This keeps one container that is thrashing, indexing, or scanning from monopolizing the host's provisioned disk throughput — on cloud VMs that budget is small (a default GCP Hyperdisk Balanced volume is ~140 MiB/s) and a single unthrottled container can freeze the entire host, including SSH.

Devices are auto-detected from /sys/block when the control plane runs containerized (compose deployments). Host/dev runs apply no limit, because a VM-backed Docker daemon (Docker Desktop) exposes different devices than the host. Set CODE_DISK_READ_LIMIT=0 to disable. Like the memory cap, changes apply to newly started containers after a control-plane restart.

Host sizing guidance

StudentsRAMCPUDiskNotes
1–316 GB4 cores30 GBDevelopment or testing
4–616–24 GB4–6 cores40 GBSmall classroom; consider lowering CODE_MEMORY_LIMIT to 3072m
7–1032 GB6+ cores50 GBFull classroom; preferred target
10+48+ GB8+ cores80 GBLarge classroom; increase CODE_MEMORY_LIMIT and cap

The Google Cloud default VM is a c4-standard-4 (4 vCPU / 15 GB RAM) with a 50 GB data disk, sized for a small-to-medium classroom. Verify the current machine type in deploy/terraform/vm.tf.

Concurrency cap

MAX_ACTIVE_CONTAINERS (default: 10) is an admission-control gate: when a student tries to open their workspace and the cap is already reached, the server returns HTTP 503 and the browser shows a "Server at capacity" toast. Students with already-running containers are not affected.

The cap can be changed at runtime without a restart; see Day-to-Day Operations.

Set a conservative cap relative to available RAM. A rough rule: multiply available RAM (after the 4 GB OS reserve) by 0.6 and divide by CODE_MEMORY_LIMIT in GB:

available_for_containers = total_RAM - 4 GB
safe_cap ≈ available_for_containers × 0.6 ÷ CODE_MEMORY_LIMIT_GB

For a 32 GB host with CODE_MEMORY_LIMIT=4096m: (32 - 4) × 0.6 ÷ 4 ≈ 4 containers with comfortable headroom for simultaneous builds. Idle editors use far less than their cap, so a higher MAX_ACTIVE_CONTAINERS usually works in practice — but expect OOM kills if many students build at once.

Measuring actual usage

# Per-container CPU and memory (one-shot)
docker stats --filter label=frc-sim.managed=true --no-stream

On the cloud VM, container_cpu_percent and container_memory_percent metrics (per workspace, sampled every 15 s) are available as Prometheus metrics and, if Grafana Cloud is configured, show trends over time there (see Grafana Cloud). Use those to confirm that your memory cap and concurrency limit are well-matched to actual student behaviour.

Disk growth

Student caches grow over time. The main contributors are:

  • Gradle cache (data/users/*/home/): grows per workspace as new dependencies are downloaded. Typically reaches 300–500 MB per student after a few builds.
  • Run logs (data/users/*/logs/): appended on every build and run.
  • Docker layer cache on the host: grows when you rebuild or update the workspace image.

Checking disk usage

# Overall free space
df -h /

# Size of the data directory
du -sh data/

# Per-workspace breakdown (approximate)
du -sh data/users/*/

Cleaning up

Remove stopped managed containers (does not affect running containers or student files):

bun run docker:cleanup

# Preview first
bun run docker:cleanup -- --dry-run

Prune Gradle caches and run logs for a specific student (stop their container first):

rm -rf data/users/<workspaceId>/home/
mkdir -p data/users/<workspaceId>/home
rm -rf data/users/<workspaceId>/logs/runs/*

Prune all regenerable data across all workspaces (containers must be stopped):

for dir in data/users/*/; do
rm -rf "$dir/home" "$dir/logs"
mkdir -p "$dir/home" "$dir/logs/runs"
done

Clean up dangling Docker images and build cache:

docker system prune -f
docker builder prune -f

Never delete data/users/*/project/; that is the student's source code. Back it up first if you are unsure (see Backups).