rCTF Docs
Overview

Scaling

Resource expectations, instance types, and horizontal scaling notes for rCTF deployments.

rCTF is intentionally lightweight. A single-node deployment of the bundled container is enough for almost any event, and horizontal scaling only kicks in at the upper end of CTF traffic.

Resource expectations#

The API process is a single Bun runtime, and the frontend is a static SvelteKit build served by nginx. The leaderboard worker runs as a Bun Worker thread inside the API process by default. PostgreSQL and Redis are external and follow their normal tuning.

In practice the platform barely touches the box it runs on. DiceCTF 2026 Quals, a large and well-attended event, ran the whole platform as a single Docker container on one Hetzner CPX62 instance (16 vCPU / 32 GiB RAM). Peak CPU across the whole machine (including the databases) sat around 1.6 cores, as shown below.

DiceCTF 2026 Quals CPU usage graph, peaking around 150% (~1.5 cores) during the event window
Warning (RAM data lost)

Memory metrics for the same window weren’t retained. Expect a similarly modest footprint, since the API process is a single Bun runtime and the leaderboard worker is a Bun Worker thread, not a separate process.

Note (Deliberately overprovisioned)

The CPX62 was definitely an overshoot. DiceCTF 2026 Quals was the first public event running rCTF v2, so if something unexpected blew up in CPU or memory during the CTF, we wanted enough runway to diagnose and patch without the platform tipping over in the meantime. A much smaller instance would have been plenty for the actual observed load. CPX62 works, but don’t treat it as a recommended baseline.

A VPS with 2 CPU cores and 4 GiB RAM is a comfortable starting point, and is what the VPS setup walkthrough targets. PostgreSQL and Redis sit alongside in the bundled compose.yml and dominate steady-state memory more than the rCTF container itself.

Instance types#

The instanceType config option (environment variable RCTF_INSTANCE_TYPE) selects what an API process runs.

ValueAPI serverLeaderboard worker thread
all (default)YesYes
frontendYesNo
leaderboardNoYes

The dispatch happens at startup in apps/api/src/index.ts:

apps/api/src/index.ts
if (config.instanceType === 'leaderboard' || config.instanceType === 'all') {
startLeaderboardWorker(logger)
}
if (config.instanceType === 'frontend' || config.instanceType === 'all') {
const port = Number(process.env.PORT ?? 3000)
Bun.serve({ port, fetch: app.fetch })
}
rctf.d/02-scaling.yaml
instanceType: frontend # or 'leaderboard' or 'all'

Horizontal scaling#

To scale beyond a single container:

  • Run any number of frontend containers behind a load balancer. They share PostgreSQL, Redis, and (with the local upload provider) a shared /app/uploads/ mount. Switching to the S3 or GCS upload provider removes that constraint.
  • Run exactly one leaderboard instance. The worker in apps/api/src/workers/leaderboard.ts schedules itself with setInterval(tick, config.leaderboard.updateInterval) and writes results to a shared Redis cache. There’s no Redis lock or leader election around tick, so multiple leaderboard instances would race on cacheLeaderboardAndGraph and waste compute recalculating the same snapshot. Concurrent ticks inside the same process are guarded by an in-process running flag, which doesn’t coordinate across processes.
Warning (Only one leaderboard worker at a time)

This includes all, which already runs a leaderboard worker. If you split into frontend + leaderboard, every other container must be frontend, never another all or a second leaderboard.

The bundled container’s all mode is plenty for most events.

Note (Forced leaderboard updates in split mode)

API mutations on frontend containers wake the separate leaderboard worker through Redis pub/sub on the leaderboard:force-update and leaderboard:recompute-challenge channels. The same hooks fire from the dynamic-scoring webhook so a frontend-only API instance can receive an external score push and still trigger an immediate leaderboard recompute on the worker instance. As long as both processes share the same Redis, forced updates land within the worker’s next tick.

Deployment templates#

There are no first-party deployment templates for horizontally scaled rCTF yet (no Helm chart, no multi-replica compose.yml, no Terraform module). It’s on the roadmap and will land here when ready. For now, the bundled single-node compose.yml is the only supported template, and splitting into frontend + leaderboard containers is something you wire up yourself against your own orchestrator.

Esc

Start typing to search the docs.