Design an online code judge
A coding-practice site lets anyone paste a program, runs it against hidden tests, and says "Accepted" or why not, in seconds. Every submission is code written by a stranger, executed on your machines. And once a week a contest makes a hundred thousand people submit at the same moment.
1. The prompt
"Design an online judge: users solve programming problems in the browser, submit code in many languages, and get a verdict from hidden test cases. Weekly contests have a live leaderboard."
What makes it hard:
- Running untrusted code. Some submissions will try to read other people's data, reach the network, or crash the machine.
- Fair timing. "Time limit exceeded" must mean the code was slow, not that a neighbour was busy.
- Peaks. Contests multiply load several times over, mostly in the final minutes.
2. Frame the problem
How many languages?
About 20: C++, Java, Python, Go, JavaScript and others.
Each needs its own compiler or runtime image and its own time-limit multiplier.
How many users and submissions?
10 M registered users; about 2 M submissions a day outside contests.
~25 submissions a second on average: modest. The contest peak is what sizes the system.
How big are contests?
Up to 100,000 participants, 90 minutes, four problems.
A known start time means capacity can be added before it begins.
How many tests per problem, and limits?
Up to ~100 hidden tests; 1–2 s CPU and 256 MB memory per test.
A submission is up to ~100 runs; most of the cost is executing tests, not the API.
How fast must the verdict come?
A few seconds normally; under 15 s during contest peaks.
Users wait with the page open, so a queue position and live progress matter.
Is there a quick "Run" with custom input?
Yes, against sample tests or the user's own input.
A separate fast lane, so practice runs don't wait behind full submissions.
What happens if a test case turns out to be wrong?
Fix it and rejudge affected submissions, contest ones included.
Submissions and test sets must be versioned, and scores recomputable.
3. What it must do
- A user can submit code and get a verdict (accepted, wrong answer, time or memory limit, runtime or compile error) with how many tests passed.
- A user can run code against sample or custom input and see the output quickly.
- Contests rank participants live by problems solved and time taken.
Not now: discussion forums and editorials (ordinary content), payments, mock interviews with video, AI hints.
4. How well it must do it
| Target | Why this number |
|---|---|
| Contained: user code can never reach the network, other submissions, credentials or the host security | One escape could leak hidden tests, user data or the whole fleet. |
| Verdict time: p95 under 5 s normally, under 15 s at contest peak speed · load shape | People wait on the page; contests compete on minutes. |
| Fair: the same code gets the same verdict, with runtimes within ~5% data quality | A flaky time limit is the most common complaint about judges. |
| Correct: never accept wrong code; every verdict is reproducible consistency | Rankings and reputations depend on it. |
| Contest availability: 99.95% during contests, leaderboard within seconds surviving failures | An outage mid-contest can't be undone. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Work per submission | Up to 100 tests × ~0.2 s CPU on average ≈ 20 CPU-seconds (less when it fails early) | Execution, not the API, is the cost → so judging runs on a separate worker fleet |
| Contest peak | 100 K people, a surge in the last 15 minutes to ~300 submissions a second | 300 × 20 CPU-s ≈ 6,000 busy cores → so we pre-scale before the start; autoscaling would arrive too late |
| Sandbox start-up | A container takes ~1 s to start; a microVM boots in under 125 ms | At 300 a second, start-up overhead matters → so we keep warm pools of sandboxes |
| Test data | 3,000 problems × 100 tests × ~1 MB ≈ 300 GB | → so test sets live in blob storage, versioned, and are cached on worker disks |
Skip estimating problem-page traffic: static content behind a CDN.
6. The nouns
- Problem, with limits and a versioned test set.
- Submission: code, language, status and verdict.
- Language runtime: a sandbox image per language.
- Contest and participant score.
problems problem_id, title, time_limit_ms, memory_mb, test_set_version
test_sets problem_id, version, object_key (blob), checker (exact | float | special program)
submissions submission_id, user_id, problem_id, contest_id?, language, code_key (blob), test_set_version,
status (queued | running | done), verdict, tests_passed, runtime_ms, memory_kb, created_at
scores contest_id, user_id, solved, penalty_minutes, last_accepted_at (source of truth; leaderboard derived)
7. The contract
POST /v1/problems/{id}/submissions {language, code} Idempotency-Key: …
→ 202 {submission_id, queue_position}
GET /v1/submissions/{id} → {status, verdict, tests_passed, runtime_ms, memory_kb}
SSE /v1/submissions/{id}/events → queued … running test 37/100 … done
POST /v1/problems/{id}/run {language, code, input} → {stdout, stderr, runtime_ms} (fast lane)
GET /v1/contests/{id}/leaderboard?page_token=… → [{rank, user, solved, penalty}]
Data flow: submit → save code and a queued row → queue by lane → a judge worker takes it → sandbox: compile, run each test with limits, compare output → verdict → update the row → notify the browser → contest scorer updates the leaderboard.
8. First working version
An API, a database and blob storage, one queue, and a pool of judge workers that run code inside containers.
8.1 Submitting and judging
The API stores the code, creates the row, and enqueues only the submission ID (Module 23). A worker loads the code and the test set, compiles, runs each test with time and memory limits, compares the output with the expected answer (or a special checker for problems with many correct answers), and stops at the first failure.
8.2 Getting the verdict
The browser listens on a server-sent events stream (Module 19) for progress ("running test 37 of 100"), falling back to polling. Verdicts are written once per submission and test-set version, so a retry never produces a second, different verdict.
8.3 Contest scores
When a contest submission is accepted, the scorer updates the participant's row (problems solved, penalty minutes) in one transaction, then the leaderboard.
9. Make it hold up
9.1 Running untrusted code safely contained
| Approach | Why | |
|---|---|---|
| ✗ Bad | Run the program as a process on the worker | It can read the worker's files and credentials, open network connections, fork until the machine dies. |
| ~ Good | A container per run: no network, read-only file system, an unprivileged user, a seccomp filter on system calls, cgroup limits on CPU, memory and process count | Stops ordinary abuse. But containers share the host kernel, so a kernel bug can be an escape. |
| ✓ Great | All of that inside a stronger boundary: a user-space kernel (gVisor) or a lightweight microVM (Firecracker), destroyed after each submission; judge workers in an isolated network with no credentials beyond reading test data | Defence in depth: an escape from the program still lands in a throwaway VM on a machine that holds nothing valuable. |
Also: never show the output of hidden tests, cap stdout size, and limit error messages so failing submissions can't print the test data out.
9.2 Fair, repeatable timing fair
- Measure CPU time of the program, not wall-clock time.
- Run one sandbox per dedicated core on identical machine types, so neighbours don't steal cache and memory bandwidth.
- Per-language time multipliers (a Python limit is several times C++'s), published with the problem.
- If a run lands just over the limit, rerun it before declaring "time limit exceeded".
9.3 Surviving the contest surge verdict under 15 s
| Approach | Why | |
|---|---|---|
| ✗ Bad | One queue, autoscaling on CPU | New machines arrive minutes after the surge; practice submissions compete with contest ones; queues explode in the final minutes. |
| ~ Good | Autoscale on queue depth, with a warm pool of sandboxes | Reacts faster and avoids start-up overhead, but still behind a step change. |
| ✓ Great | Pre-scale the contest fleet before the start (the schedule is known), give contests their own queue and workers, keep warm sandboxes, allow one pending submission per user, and show queue position | Capacity is there before demand; practice can't slow the contest; users see progress instead of retrying. Try it in section 13. |
9.4 Test data and runtimes on every worker verdict time
- Test sets are immutable, versioned objects in blob storage; workers cache them on local disk by version and pre-load the contest's problems before it starts.
- Language images are pre-pulled onto every worker; a new language version is a new image, rolled out gradually and rejudged against a sample of past submissions first.
- Each submission records the test-set version it was judged against, so every verdict can be reproduced.
9.5 A live leaderboard for 100,000 people leaderboard within seconds
- Rank by problems solved (more is better), then penalty time (less is better). Encode both into one sortable score and keep it in a sorted set (Module 09; Design D20): updates and "my rank" are logarithmic.
- The
scorestable stays the source of truth; the sorted set can be rebuilt from it after a rejudge or a cache failure. - Serve the top pages from a cache refreshed every few seconds; many contests freeze the public board in the final hour.
9.6 Rejudges and cheating correct
Rejudge after fixing a test: create a new test-set version, re-queue affected submissions on a low-priority lane, recompute scores from verdicts, rebuild the leaderboard. Plagiarism is checked after the contest by fingerprinting normalised code (stripping names and whitespace) and comparing submissions per problem; matches go to human review.
9.7 The whole design after the deep dives
10. Interviewer follow-ups
Someone escapes the sandbox.
Defence in depth limits it: the microVM is thrown away, the worker holds no credentials and can't reach other networks, and workers are rebuilt from images regularly. Detect it with system-call auditing and alerts on unexpected network attempts; patch and rotate the fleet.
Add an interactive problem (the program talks to a judge program).
Run the interactor in a second sandbox, connected to the user's program by pipes only, with a time limit on the whole exchange. The checker becomes a program too, versioned with the test set.
A test case was wrong during a contest.
Publish a new test-set version, rejudge every contest submission for that problem on a dedicated lane, and recompute scores from the database. Announce it; freeze the board until it's done.
Ten times the contest size.
The fleet is the only part that scales with it: pre-scale more cores (or several contest pools by region), stagger the start by region if needed, and keep one pending submission per user to stop retry floods.
Cut the bill.
Practice judging on cheaper interruptible capacity (spot instances: a lost run is just re-queued), contests on reserved capacity, early exit on the first failing test, and caching of compiled standard libraries in images.
Show "your runtime beats 87% of submissions".
Keep a per-problem, per-language histogram of accepted runtimes, updated in the background; look up the percentile when a verdict arrives. It's approximate by design.
Users in Asia see slow verdicts.
Run judge fleets in several regions, routing submissions to the nearest; test data and images are replicated. Contest scores still go to one primary database (or one per contest).
Stop people retrying the same submission 20 times.
One pending submission per user per problem, an idempotency key on submit, and identical-code detection that returns the earlier verdict.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| Worker dies mid-judging | A slightly slower verdict | Visibility timeout expires | The submission is re-queued; verdicts are idempotent per version |
| Queue backlog in a contest | Longer wait, with queue position | Queue depth and oldest-message age | Pre-scaled fleet; contest lane isolated; one pending per user |
| Bad language image | Wrong compile errors | Canary rejudge of known submissions | Staged rollout of images; instant rollback |
| Wrong test case | Correct code rejected | Reports; unusual failure rates on one test | Versioned test sets and rejudges |
| Leaderboard cache lost | Board briefly stale | Board age | Rebuild from the scores table |
| Abuse attempt | Nothing | Blocked system calls, network attempts | Sandbox layers; ban the account |
12. Wrap-up and the clock
Trade-offs we made:
- Strong isolation (microVMs) at some cost in overhead, offset by warm pools.
- Dedicated cores per run for fairness, at lower density.
- Pre-scaling for known peaks rather than trusting autoscaling.
- Separate lanes for contests, practice and "Run".
- A derived leaderboard, rebuilt from the scores table when needed.
What we'd watch: verdict latency by lane · queue depth and oldest age · sandbox start time · runtime variance on reference solutions · blocked-syscall alerts · leaderboard age.
At 10×: more pre-scaled capacity and regional fleets; the API, database and leaderboard barely change.
A 60-minute interview
| 0–5 | Frame: languages, limits, contest size, verdict time |
| 5–10 | Must do, how well, CPU-seconds per submission |
| 10–15 | Nouns, contract, data flow |
| 15–25 | First version: submit → queue → worker → verdict |
| 25–50 | Deep dives: the sandbox (9.1), fairness (9.2), the surge (9.3) |
| 50–60 | Leaderboard or rejudges, follow-ups, wrap-up |
A 90-minute interview
| 0–8 | Frame, including rejudges and fairness |
| 8–15 | Must do, how well, numbers |
| 15–22 | Nouns, contract, data flow |
| 22–35 | First working version |
| 35–75 | Deep dives 9.1–9.6 and the final picture |
| 75–85 | Follow-ups: escape, interactive problems, cost |
| 85–90 | Trade-offs, metrics, 10× |
| Level | What a strong answer shows |
|---|---|
| Mid-level | Async judging through a queue and workers, containers with limits, a job status the browser polls or streams, a sorted-set leaderboard. |
| Senior | Layered sandboxing (seccomp, cgroups, no network, microVMs), CPU-time fairness, pre-scaling and separate lanes for contests, versioned test sets and reproducible verdicts. |
| Staff+ | Threat model and defence in depth, warm-pool economics, rejudge and recompute strategy, capacity planning for known peaks, operational readiness for contest day. |
Common mistakes:
- Running user code in the API server, or as a plain process.
- Trusting a container alone as the security boundary.
- Wall-clock time limits on shared machines.
- "We'll autoscale" for a peak whose start time you know.
- A leaderboard with no source of truth behind it.
- Printing hidden test data in error messages.
13. Try it, check yourself, read more
Contest-day simulator. A 90-minute contest: 60 submissions a second, surging to 300 a second in the last 15 minutes, on top of 25 a second of practice. Each submission needs 20 CPU-seconds, spread over parallel test runs (about 3 s of wall time when there's no queue). Pick how the fleet behaves and watch the wait for a verdict (red) and the fleet size (dashed).
Model: autoscaling adds up to 15% capacity a minute after a 3-minute delay; a cold sandbox adds 3 CPU-seconds and 2 s of start-up to each submission; with separate lanes, contest submissions go first and practice uses the spare capacity. Illustrative numbers.
Why isn't a container with seccomp and no network enough on its own?
What makes time limits fair?
The contest starts at 15:00 sharp. How do you avoid a verdict backlog?
How can every verdict be reproduced months later?
| Resource | Why |
|---|---|
| Firecracker: Lightweight Virtualization for Serverless Applications (NSDI 2020) | How microVMs give VM isolation with container-like start-up. |
| Firecracker and gVisor | The two common stronger boundaries for untrusted code. |
| nsjail | A process jail combining namespaces, cgroups and seccomp, used for exactly this kind of workload. |
| seccomp(2) and cgroup v2 | The Linux building blocks for system-call filtering and resource limits. |
| Judge0 | An open-source code execution system to read and run. |