System Design

Design an Online Judge

Step 1 / 9

Learn system design by building a code-execution judge like LeetCode, Codeforces or HackerRank's backend step by step.

The numbers to beatasyncsubmission accepted instantlyworker poolscales with contest load0 blockingon the API request path

Deep cut · 14:19

Same forty lines, two verdicts — what actually changed?

The interactive walkthrough above covers the submit path, the sandbox, hidden test cases and checkers, resource limits, determinism and fair queueing. The film is told on the judge's own screen — a submission card travelling a time axis, boxes born and destroyed, a queue filling and being capped — and carries one number along the whole way: the spread between two runs of identical code. It opens at 343 milliseconds and ends at 4.

  • See why the obvious build is not merely slow but wrong: the same forty lines pass at 997ms against a limit of 1000, then fail the next morning at time limit exceeded. Nothing about the code changed. The machine got busier, and a stopwatch measures the room, not the runner.
  • See the decision everything rests on: stop counting seconds and count the work. CPU time is the slices the kernel actually gave your process — so waiting for a turn does not count, and being paused does not count. That one swap is what collapses the 343ms spread to 4.
  • See what a box has to guarantee: no network, 256MB, 32 processes, destroyed after one run — so four lines that fork forever take the box instead of the website, and a program that never stops is stopped by something that is not the program.
  • Take it into the interview: a queue behind a 90ms endpoint, an ephemeral sandbox, hard limits, CPU-time accounting, hidden tests with a checker instead of a text compare, three submissions in flight per person, and one pinned image — each bought by a number, with what it costs said out loud, including the fact that your answer is now a promise rather than an answer.

In the interview room

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

Functional requirements

What it must do — agree on these before drawing a single box.

  • Submit: accept a code submission and hand it off — the API never executes anything itself.
  • Sandbox: run untrusted code in strict isolation — no network, hard CPU/memory limits, torn down after each run.
  • Judge: run against hidden test cases (exact match or a custom checker) and record a verdict (AC/WA/TLE/…).
  • Limit fairly: cap CPU time (not wall-clock) so a verdict is consistent under any host load.
  • Queue fairly: a durable async queue plus a per-user fair share so a contest burst can’t starve anyone.

Non-functional requirements

The qualities that shape the whole design — each one names the mechanism that buys it.

Untrusted code never compromises the host
Layered sandboxing — CPU/memory limits (cgroups), no network, restricted filesystem, process/fork caps — together, with an ephemeral environment destroyed after each run.
The API stays fast during a contest burst
A durable Job Queue decouples "accepted" from "judged"; a worker pool pulls asynchronously and scales independently of submission rate.
Valid alternative answers aren’t wrongly rejected
A custom checker validates the correctness criterion (float tolerance, multiple valid orderings) instead of a literal diff, short-circuiting on the first failing test.
A verdict is fair regardless of host load
Limit actual CPU time consumed via cgroups accounting, not wall-clock — the same algorithm gets the same outcome alone or under a thousand submissions.
The same code gets the same verdict every time
CPU-time limits remove host contention as a variable, plus freshly-provisioned ephemeral sandboxes and an explicit rejudge for disputed cases.
One user can’t starve a live contest
Fair queueing gives each user a bounded share of worker throughput (a per-user in-flight cap), with autoscaling absorbing the aggregate spike.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Sandbox every submissionover running it inline on the API server

Executing untrusted code unisolated on shared production infrastructure is a full arbitrary-code-execution hole — it can read other users’ data, attack the network, or fork-bomb the host. Nothing else matters until this is solved.

Layered isolation + ephemeral teardownover a plain container with defaults

A default container still shares the kernel, caps nothing, and can open the network or fork endlessly. CPU/memory limits, no network, restricted FS, process caps and a thrown-away env together mean a gap in one is contained by the rest.

Async durable queueover blocking the request until judging finishes

Holding thousands of HTTP connections open on multi-second executions makes the API the bottleneck. A queue lets it accept as fast as submissions arrive while worker capacity scales to the burst independently.

CPU-time limitsover a wall-clock timeout

Wall-clock is contaminated by host contention — the same submission can pass under light load and fail under heavy load through no fault of the code. Measuring actual CPU consumed keeps verdicts fair and consistent.

Per-user fair queueingover strict global FIFO

Global FIFO lets one user rapid-firing near-identical attempts consume near-term capacity that belongs to everyone. A bounded per-user share keeps judging turnaround even across a shared contest window.

What this teaches

Learn system design by building a code-execution judge like LeetCode, Codeforces or HackerRank's backend step by step. An interactive guide covering why running submitted code inline is a security catastrophe, what real sandboxing has to guarantee beyond just a container, async queue-based execution for contest-scale bursts, verdict determination across hidden test cases, CPU-time (not wall-clock) resource limits for fairness, judge determinism, and fair queueing so one flood of submissions can't starve everyone else during a live contest.

Key takeaways

  • Running untrusted code unsandboxed on shared infrastructure is a full security vulnerability — nothing else matters until this is solved.
  • Real sandboxing layers CPU/memory limits, no network access, restricted filesystem access, and process caps together, with a fully ephemeral environment per run.
  • A durable job queue decouples "submission accepted" from "execution finished," letting worker capacity scale independently of submission rate.
  • Verdicts come from comparing output against hidden test cases — either exact match or a checker program for problems with multiple valid outputs.
  • CPU-time limits (via cgroups), not wall-clock time, keep verdicts fair and consistent regardless of host contention.
  • Determinism is engineered by removing sources of non-determinism one at a time — CPU-time limiting, ephemeral sandboxes, and rejudge for disputed cases.
  • Fair, per-user queueing prevents one user's submission volume from starving everyone else's judging turnaround during a live contest.

Concepts covered

  • What is an online judge, specifically?
  • Run the code right here
  • A container alone isn't enough
  • Submission and execution are two different moments
  • Hidden tests, checkers, and short-circuiting
  • CPU time, not wall-clock
  • The same submission should get the same verdict, always
  • One user shouldn't starve everyone else
  • Escape attempts, queue backlogs, and cleanup

Design an Online Judge — read the full walkthrough as text

the same steps, decisions & trade-offs, for reading, reference & search

The big idea

What is an online judge, specifically?

Users submit arbitrary code — sometimes buggy, sometimes deliberately malicious — and expect it to be run, measured, and graded, at contest scale, fairly, without ever letting one submission compromise the host, starve other users, or get an inconsistent verdict on a rerun.

We'll build it around the one property that has to hold above all others: untrusted code execution must never become a security or fairness incident. Everything — sandboxing, queueing, resource limits, fair scheduling — flows from that.

How to read this: Each step opens with a real design decision — make the call before I show you what ships. Watch the diagram grow, hover the boxes, and at the end kill the sandbox workers and flood a contest-start burst to see what survives. Hit Begin.

Step 1 · The baseline

Run the code right here

Simplest version: the API server receives a submission and just... runs it, directly, in-process or as a plain subprocess, to get the output. Why is this dangerous?

Design decision: The API server directly executes arbitrary user-submitted code. What's the danger?

The call: It's a full arbitrary-code-execution vulnerability on shared, production infrastructure: a malicious submission could read other users' data, attack the network, exhaust host resources, or compromise the server outright. — Running untrusted code with no isolation on the same infrastructure serving every other user's requests means a single malicious submission can potentially read filesystem data it shouldn't, make outbound network calls, fork-bomb the host, or otherwise compromise a shared production system — this isn't a hypothetical, it's the defining risk of the entire product category.

Running submitted code directly, with no isolation, on infrastructure shared with everything else is a full arbitrary-code-execution vulnerability. Nothing else in this design matters if this foundational problem isn't solved first: untrusted code must never run unsandboxed on shared infrastructure.

This system's entire job is running untrusted code safely: Unlike most systems where security is one concern among many, an online judge's core PRODUCT FUNCTION is executing arbitrary, potentially adversarial code — security isn't a bolt-on feature here, it's the central engineering problem.

Step 2 · Real sandboxing

A container alone isn't enough

Isolate execution in some kind of sandbox — a container, a VM, a purpose-built sandbox runtime. But a plain container with default settings still shares the host kernel, has no CPU/memory ceiling by default, and can still open network connections or fork endlessly. What does a sandbox actually need to guarantee?

Design decision: What does "sandboxed execution" actually need to guarantee, beyond just running in a container?

The call: Hard CPU-time and memory limits (via cgroups), no network access, filesystem access limited to only what's needed, and process/fork limits — combined, not any single one alone — plus destroying the sandbox environment after every single run. — Real isolation needs multiple, layered guarantees together: bounded CPU time (kills runaway loops), bounded memory (kills memory exhaustion), no network (prevents exfiltration or attacking other systems), restricted filesystem access, and a cap on process/thread creation (prevents fork bombs) — and the whole environment is thrown away after each run so nothing persists between submissions.

Real sandboxing means several guarantees enforced together: hard CPU-time and memory limits via cgroups, no network access, minimal filesystem access, a cap on process/fork creation (stopping fork bombs), and a fully ephemeral environment destroyed after every single run so nothing persists or leaks between submissions.

Defense in depth: No single isolation mechanism is sufficient on its own against a sufficiently adversarial submission — the guarantees have to be layered, so a gap in any one (say, a container-escape bug) is still contained by the others (no network, capped resources, ephemeral teardown).

Step 3 · Async execution at contest scale

Submission and execution are two different moments

Execution can take real time — seconds, sometimes more for slow submissions — and during a live contest, thousands of submissions can land within the same few seconds. Should the API request block until execution completes?

Design decision: A contest produces thousands of near-simultaneous submissions, each taking real time to execute. Block the request until done?

The call: Accept the submission immediately, place it on a durable Job Queue, and let a pool of Sandbox Workers pull from that queue asynchronously — the client polls or gets pushed the verdict once judging completes. — Decoupling "accepted" from "judged" via a durable queue means the API can accept submissions as fast as they arrive, completely independent of how many workers are currently busy executing code. Worker capacity can scale up during a contest burst and down afterward, without the API path itself ever being the bottleneck.

Accept submissions immediately and place them on a durable Job Queue; a pool of Sandbox Workers pulls and executes asynchronously, independent of submission rate. This decouples "accepted" from "judged" — the API stays fast and available even during a massive contest-start burst, and worker capacity scales to match demand.

Decouple the fast path from the slow path: The same pattern as ride matching's batch window and the logging pipeline's local buffering: whatever takes real time (sandboxed execution, here) shouldn't block the fast, cheap operation (accepting a submission) — a queue in between lets each scale independently.

Step 4 · Determining the verdict

Hidden tests, checkers, and short-circuiting

A submission runs against multiple hidden test cases. Some problems have exactly one correct output per input (exact match); others have multiple valid outputs (floating-point tolerance, multiple valid orderings). How does the judge decide pass or fail?

Run the submission against each hidden test case in Test Cases, comparing actual output to expected — either an exact match, or, for problems where multiple outputs can be valid, a custom checker program that validates correctness rather than doing a literal diff. The judge typically short-circuits on the first failing test (reporting that specific verdict — Wrong Answer, Time Limit Exceeded, Memory Limit Exceeded, Runtime Error) rather than always running every test, saving compute on submissions that are already known to fail.

Not every correctness check is a string comparison: For many real problems, "correct" isn't a single fixed string — a checker program encodes the actual correctness CRITERION (does this satisfy the constraints, is it within floating-point tolerance) rather than relying on exact text matching, which would incorrectly reject valid alternative answers.

Step 5 · Precise, fair resource limits

CPU time, not wall-clock

Enforce a time limit on each submission. If the judge is under heavy load (many workers executing simultaneously, contending for the same host's CPU), a wall-clock timeout can unfairly penalize a submission that's algorithmically fine but simply got less CPU time due to contention. What should the time limit actually measure?

Design decision: The judge is under heavy contest load. What should the time limit for each submission actually measure?

The call: CPU time actually consumed by the process (via cgroups CPU accounting), independent of how much wall-clock time passed while it may have been waiting for a CPU slice. — CPU-time limits (enforced via cgroups) measure the actual computational work done, regardless of how contended the host was at that moment — a submission that needs 500ms of CPU work gets judged on that 500ms whether it ran alone or alongside a thousand other contest submissions, keeping verdicts fair and consistent.

Enforce limits on actual CPU time consumed (via cgroups CPU accounting), not wall-clock duration. This keeps a submission's verdict consistent and fair regardless of how contended the host happens to be at that moment — the same algorithmic solution gets the same time-limit outcome whether it runs alone or during the busiest minute of a contest.

Measure what you actually care about: "How much real computational work did this do" (CPU time) is the meaningful, fair signal for judging efficiency; "how long did a human waiting on a clock perceive it took" (wall-clock time) is contaminated by factors — host load, scheduling — that have nothing to do with the submission's own correctness or efficiency.

Step 6 · Determinism

The same submission should get the same verdict, always

A judge that gives a different verdict on a rerun of the exact same code — sometimes Accepted, sometimes a flaky Time Limit Exceeded — destroys trust, especially in a competitive contest setting where a verdict has real stakes.

CPU-time limiting (Step 5) already removes the dominant source of flakiness — host contention no longer affects the measured time. Combine it with fully ephemeral, freshly-provisioned sandboxes per run (no shared, potentially-degraded state carried between submissions) and, for genuinely borderline cases, an explicit rejudge mechanism that reruns a submission under controlled conditions if its verdict is disputed. Determinism isn't achieved by hoping the system behaves consistently — it's achieved by removing every source of non-determinism one at a time.

Determinism is engineered, not assumed: A judge's credibility depends on "the same input always produces the same output." Every design decision so far (CPU-time not wall-clock, ephemeral sandboxes, isolated resource limits) is, among other things, in service of this one property.

Step 7 · Fair queueing under contest load

One user shouldn't starve everyone else

A contest opens. Some users submit rapidly (retrying slightly different solutions), others submit once and wait. Should the Job Queue be strictly first-come-first-served across ALL submissions from ALL users?

Design decision: During a contest, should worker capacity be allocated strictly first-come-first-served across everyone's submissions?

The call: Give each user a fair, bounded share of worker throughput — a per-user queue or rate limit within the overall queue — so one user's submission volume can't crowd out everyone else's judging turnaround during a shared contest window. — Per-user fairness (a fair-queueing discipline, or a rate limit on how many of one user's submissions can be "in flight" being judged at once) ensures worker capacity is distributed roughly evenly across active users, regardless of how many times any single user has submitted — closer to what contest participants actually expect as "fair."

Implement fair queueing: each user gets a bounded share of worker throughput — a per-user submission-in-flight cap or a fair-share scheduling discipline within the overall queue — so rapid resubmission by one user can't crowd out judging turnaround for everyone else during a shared, high-stakes contest window. Combine this with worker autoscaling to absorb the aggregate burst.

Global throughput and per-user fairness are different goals: Maximizing total submissions-judged-per-second (global throughput) and ensuring no single user dominates that throughput (per-user fairness) can conflict — a fair-queueing discipline is specifically designed to balance both, rather than optimizing raw throughput alone at fairness's expense.

Step 8 · The sharp edges

Escape attempts, queue backlogs, and cleanup

Real-world stress tests: a submission deliberately trying to escape the sandbox, a queue backlog large enough that even fair-queued wait times become noticeable, and making sure every sandbox is genuinely, completely cleaned up between runs.

Defense in depth (Step 2) means an escape attempt has to defeat CPU/memory limits, no-network, restricted filesystem access, AND process caps simultaneously — a much higher bar than defeating any single mechanism. Queue backlogs are absorbed by worker autoscaling first, and, if a backlog still grows past a threshold, users see an honest estimated wait rather than a silent stall. And every sandbox is provisioned fresh and fully destroyed — not reset, not reused — after each run, so there's no path for one submission's leftover state to affect the next.

Design for the unhappy path: Worker outage → queue holds everything safely, nothing lost. Contest burst → fair queueing plus autoscaling. Escape attempt → layered isolation, not a single point of failure. A judge that only works for well-behaved code under light load is a demo; one that stays secure, fair, and consistent under adversarial and contest-scale conditions is a product.

You did it

You just designed an online judge.

  • Running untrusted code unsandboxed on shared infrastructure is a full security vulnerability — nothing else matters until this is solved.
  • Real sandboxing layers CPU/memory limits, no network access, restricted filesystem access, and process caps together, with a fully ephemeral environment per run.
  • A durable job queue decouples "submission accepted" from "execution finished," letting worker capacity scale independently of submission rate.
  • Verdicts come from comparing output against hidden test cases — either exact match or a checker program for problems with multiple valid outputs.
  • CPU-time limits (via cgroups), not wall-clock time, keep verdicts fair and consistent regardless of host contention.
  • Determinism is engineered by removing sources of non-determinism one at a time — CPU-time limiting, ephemeral sandboxes, and rejudge for disputed cases.
  • Fair, per-user queueing prevents one user's submission volume from starving everyone else's judging turnaround during a live contest.
built so a fork bomb never leaves the sandbox and one user never starves the whole contest — make the calls, flood a contest start, run the gauntlet.
Finished this one? 0 / 65 System Designs done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More System Designs