Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

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

The whole design, in writing

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

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.

Usersubmits code
New in this step: User.

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.

What the new pieces do

Userclient
Submits a solution and waits — sometimes for milliseconds, sometimes, during a packed contest, for longer — to find out if it passed.

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?

Usersubmits codeSubmission APIintake
New in this step: Submission API.

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

  1. Performance is a real but secondary concern here — the primary issue is far more severe than speed.

  2. 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.

  3. CPU cost for legitimate code is a real, separate concern (addressed later via resource limits) — it's not the primary danger of running UNISOLATED, potentially malicious code on shared infrastructure.

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.

Why this piece earns its place

The tempting middle ground is to keep running submissions on the API host and just refuse the dangerous ones — scan the source for socket, exec, open, block a list of imports, reject anything that looks hostile. That loses, and it’s worth saying why out loud: a denylist is a claim that you can enumerate every route a Turing-complete language has to the operating system, made against people whose whole hobby is finding the one you missed. You only learn it was incomplete from the incident. Isolation is the opposite shape of claim — it doesn’t care what the code tries, because the capability isn’t there to reach for. The other half people skip is the build. Compiling is already running untrusted input through a toolchain: includes, macros, template expansion and build scripts all execute at compile time, and a submission can exhaust memory or spin forever before a single line of its own program runs. So the boundary isn’t “sandbox the program”, it’s “sandbox everything that touches the submission”, compiler included.

What the new pieces do

Submission APIgateway
Accepts a submission and hands it off — never executes anything itself.

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?

Submission APIintakeSandbox Workersisolated execution
New in this step: Sandbox Workers.

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

  1. A default container alone doesn't cap CPU time, doesn't cap memory, doesn't block network access, and doesn't prevent a fork bomb from exhausting host process limits — process isolation is necessary but nowhere near sufficient by itself.

  2. A wall-clock timeout alone stops an infinite loop eventually, but does nothing about memory exhaustion, network access, filesystem access outside the sandbox, or a fork bomb consuming process-table entries before the timeout even fires.

  3. 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.

Why this piece earns its place

Ephemeral teardown quietly assumes a fresh environment is cheap to create. It isn’t — provisioning a sandbox, its filesystem and its toolchain can cost as much as the run it wraps, and that cost lands on the critical path of every verdict, worst of all in the minute a contest opens. The pressure that creates is the dangerous one: someone proposes keeping a sandbox warm between submissions, and single-use isolation is gone. The way out is to pre-create rather than reuse. Keep a pool of fresh, never-used sandboxes, hand one out per submission, and refill the pool asynchronously behind the burst — each sandbox still serves exactly one run and is destroyed after it, only the waiting moved earlier. It’s also worth naming this layer’s ceiling honestly: cgroups, namespaces and seccomp filters all rest on one shared kernel, so a single kernel bug defeats several of them at once. Where the threat model justifies the cost, the answer is a separate kernel per run — a microVM — not more flags on the same one.

What the new pieces do

Sandbox Workersservice
Runs submitted code in strict isolation — no network, hard CPU/memory limits, torn down after every single run.

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?

Submission APIintakeSandbox Workersisolated executionJob Queuedurable, async
New in this step: Job Queue.

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

  1. Holding open thousands of simultaneous HTTP connections, each waiting on a multi-second sandboxed execution, doesn't scale — the API server itself becomes the bottleneck, tied up waiting rather than accepting the next submission.

  2. 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.

  3. Simply rejecting submissions during exactly the highest-traffic, highest-stakes moment (a live contest) is the worst possible user experience — the system should absorb the burst, not refuse it.

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.

Why this piece earns its place

Accepting instantly turns a request into a promise, and a queue is allowed to break that promise quietly. Durable delivery is at-least-once, not exactly-once: a worker that dies mid-execution gets its job redelivered, which is exactly what you want — right up until the submission is the reason the worker died. Then it kills the next worker, and the next, and one adversarial program walks the entire pool while looking like an infrastructure problem. So bound it: count deliveries on the job itself, and after a small number stop redelivering and record an explicit system-error verdict with an alert, rather than letting it circle forever. At-least-once also means two workers can legitimately judge the same submission at once, so the verdict write is keyed by submission id and idempotent instead of appended. And because a genuinely lost job produces no error anywhere — just a submission that says judging forever — I’d age the pending set and alarm on anything older than the worst plausible judging time, since nothing else will tell you.

  • asyncsubmission accepted instantly
  • worker poolscales with contest load
  • 0 blockingon the API request path

What the new pieces do

Job Queueevents
Decouples "submission accepted" from "execution finished" — nothing is lost even if every worker is briefly unavailable.

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?

Sandbox Workersisolated executionTest Caseshidden + checkersVerdictsAC / WA / TLE / …
New in this step: Test Cases, Verdicts.

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.

Why this piece earns its place

Short-circuiting makes the verdict a function of test order, which is a contract rather than an implementation detail. A submission that’s wrong on one test and too slow on another gets Wrong Answer or Time Limit Exceeded purely according to which hidden test the runner reached first. That’s fine while the order is fixed — but it means test order is part of the judge’s state. Shuffling the tests, inserting a case at the front, or racing tests in parallel can change a published verdict for code nobody touched, and that shows up later as exactly the flaky-looking rerun a judge can’t afford. So the order is stored and versioned with the test set, and parallel execution still reports the lowest-index failure rather than the first one to finish. The place the assumption stops holding altogether is partial scoring: once a problem awards points per subtask, every subtask has to be attempted, and “stop at the first failure” shrinks to an optimization applied inside a subtask that has already failed.

What the new pieces do

Test Casesdata
The hidden inputs, expected outputs, and (where needed) custom checker logic that actually determines a verdict.
Verdictsdata
The durable record of every submission's outcome — what the user ultimately sees, and what leaderboards are built from.

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?

UserSubmission APISandbox WorkersJob QueueTest CasesVerdicts
The system as it stands at this step. · swipe to pan the diagram

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

  1. Wall-clock time is affected by host contention: the same submission can take noticeably longer under heavy concurrent load purely because it received less CPU time, not because its algorithm is actually slower — this makes verdicts inconsistent and unfair depending on unrelated system load.

  2. 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.

  3. An unbounded execution time means an infinite loop (or a deliberately slow submission) never terminates, tying up a sandbox worker indefinitely and defeating the entire point of a time limit as a correctness/efficiency signal.

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.

Why this piece earns its place

The limit is a number a human chooses, per problem, and choosing it is most of the work. The usual method is to measure the intended solution’s CPU time on judge hardware and multiply — enough headroom that a competent implementation of the right algorithm passes comfortably, not so much that the wrong complexity slips through. Both directions cost something real. Too tight and the problem stops testing algorithms and starts testing constant factors and language choice: the same correct approach passes compiled and fails interpreted, which is why judges carry per-language multipliers instead of one global number. Too loose and the problem loses its teeth — a quadratic solution passes where a linearithmic one was the entire point, and the problem no longer distinguishes what it was written to distinguish. The consequence for problem setting is that intended and unintended complexities should be separated by a wide margin at the largest input, because any submission sitting right on the limit is being decided by noise no matter how cleanly you account for the CPU.

  • CPU-time limitvia cgroups accounting
  • contention-independentsame verdict under any load
  • memory limitreal OOM kill, not a soft check

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.

UserSubmission APISandbox WorkersJob QueueTest CasesVerdicts
The system as it stands at this step. · swipe to pan the diagram

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.

Why this piece earns its place

There’s one source of non-determinism left that fresh sandboxes don’t touch: the environment is new every time, but it isn’t necessarily the same. Compiler version, optimization flags, standard library, base image, kernel and the limit configuration all feed the outcome, and all of them get upgraded. Bump the compiler and code that just fit inside its limit can fall outside it — same submission, same judge, different verdict, months apart. That bites hardest on the mechanism this step just introduced, because a rejudge run under today’s toolchain isn’t a rerun of the original judgement, it’s a fresh one. So I’d version the judge environment as a single artifact — image digest, compiler versions, limit config — and stamp every verdict with the version that produced it. A rejudge then means rerunning under the recorded version, and upgrading becomes a deliberate event with a decision attached: leave old verdicts alone, or mass-rejudge a problem set under the new one. That choice is a policy call about standings, and it wants answering before a contest rather than during an appeal.

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?

Sandbox WorkersCPU-time limitedJob Queuedurable, asyncVerdictsAC / WA / TLE / …Fair Queueingper-user share
New in this step: Fair Queueing.

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

  1. Strict global FIFO actually allows an unfair outcome: a user who happens to submit rapidly (retrying many near-identical attempts) can consume a disproportionate share of near-term worker capacity, delaying users who each submitted only once but happened to submit a moment later.

  2. 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."

  3. Random shuffling doesn't target the actual fairness problem (one user consuming disproportionate share) — it just adds unpredictability without guaranteeing any particular user gets a fair, bounded share of throughput.

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.

Why this piece earns its place

A per-user cap has an obvious setting that’s wrong. One submission in flight per user is maximally fair and hurts everybody, because it idles workers: if a handful of users are active and most worker slots are free, a strict cap leaves them doing nothing while jobs sit in the queue — fairness paid for by throwing away capacity nobody was competing for. So the share has to be work-conserving. The cap binds only while there’s contention; when workers would otherwise go idle, a user is let past it. A fair share is a floor on what you’re guaranteed when everyone wants capacity, not a ceiling on what you may use when nobody does. What’s left to tune is how hard it binds under contention, and turning that down slides you smoothly back toward the FIFO this step just rejected. The assumption underneath is that the account is the unit of fairness, which holds only while accounts are expensive to create — otherwise the key has to be something with a cost attached, like contest registration, or three sock puppets beat the discipline outright.

What the new pieces do

Fair Queueingservice
Ensures one user's submission flood can't starve worker capacity that the rest of a live contest needs.

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.

UserSubmission APISandbox WorkersJob QueueTest CasesVerdictsFair Queueing
The system as it stands at this step. · swipe to pan the diagram

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.

Why this piece earns its place

Cleanup is the one step that runs after the adversary has had the machine, which makes “destroyed after every run” the claim most likely to be quietly false. A submission can leave processes that outlive the one the worker spawned, hold a mount busy so it won’t unmount, or fill the disk the sandbox was carved out of. If teardown is best-effort code inside the same worker that just ran the program, that failure is silent — a line in a log, and the worker picks up the next job on a host that is no longer clean. Two things make the claim real. Teardown happens at the boundary and from outside: kill the whole cgroup and drop the namespace and its filesystem with it, so there’s nothing to enumerate and nothing to overlook. And a teardown that doesn’t confirm is a fatal worker error, not a warning — the worker stops taking jobs, the one it was holding goes back for redelivery, and the host is recycled. A judge that can’t prove the last sandbox is gone has no business starting the next one.

You did it

You just designed an online judge.

UserSubmission APISandbox WorkersJob QueueTest CasesVerdictsFair Queueing
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • 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.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why is CPU-time limiting specifically important for FAIRNESS, not just correctness?

    Correctness (does the algorithm terminate, does it respect its intended complexity) could arguably be checked with any reasonably generous time limit. Fairness is the sharper issue: in a live contest, hundreds of submissions may be judged concurrently on shared hardware, and WALL-CLOCK time is directly affected by how much CPU contention exists at that exact moment — meaning the same submission could plausibly pass under light load and fail under heavy load, through no fault of the code itself. CPU-time limiting removes contention as a variable entirely, so a submission's verdict depends only on its own algorithmic efficiency, which is the actual property a contest is trying to measure.

  2. Could a submission attempt a "timing side-channel" attack to learn something about the sandbox or other users' data, even within CPU/memory limits?

    This is exactly the kind of concern defense-in-depth is meant to address at every layer rather than just one: no network access prevents exfiltrating anything learned to an external endpoint, restricted filesystem access prevents reading anything beyond what a submission legitimately needs, and full ephemeral teardown after every run means there's no PERSISTENT state (from a previous submission, or another user's data) present in the sandbox to observe in the first place. A timing side-channel could theoretically still leak information about the sandbox's OWN execution characteristics, but with no persistent secrets or other users' data ever present in an ephemeral, isolated environment, there's structurally very little of value for such an attack to actually extract.

  3. How would you handle a problem where the "correct" output legitimately depends on floating-point precision, where exact string matching would incorrectly fail valid answers?

    This is precisely what a custom checker program (Step 4) is for: instead of a literal string/byte comparison between actual and expected output, the checker parses the submission's output and validates it against the problem's actual correctness criterion — e.g. "is this floating-point value within 1e-6 of the expected value" rather than "is this text byte-for-byte identical to the expected text." The checker encodes domain-specific correctness logic per problem, which is strictly more expressive than exact-match diffing and is standard practice for any problem where legitimate answers can vary (floating point, multiple valid orderings, "any one of several correct outputs is acceptable").

  4. Why autoscale workers rather than just always provisioning enough capacity for peak contest load?

    Contest traffic is extremely bursty — a huge spike right at contest start and end, with far lower baseline load the rest of the time — so permanently provisioning for peak capacity means paying for mostly-idle workers the vast majority of the time. Autoscaling lets the worker pool grow rapidly right before and during a scheduled contest (a predictable event, so scaling can even be pre-warmed ahead of a known contest start time) and shrink back down afterward, matching cost to actual demand rather than to the rare peak — the same elastic-scaling principle used across most bursty, predictable-peak workloads.

  5. What's the relationship between fair queueing (Step 7) and rate limiting seen in other systems in this series?

    They're close cousins solving a similar shape of problem — preventing one actor from consuming disproportionate shared resources — but with a different emphasis. A simple rate limit (like a token bucket) caps how FAST one user can act, uniformly, regardless of what everyone else is doing. Fair queueing is explicitly RELATIVE: it ensures each active user gets a roughly even SHARE of currently-available capacity, which can flex based on how many other users are competing for it right now. During a quiet period, a single user might reasonably get to submit rapidly with little contention; during a contest-start burst with thousands of concurrent users, that same user's effective throughput is automatically squeezed down by the fair-share discipline — a property a flat, non-relative rate limit doesn't inherently provide.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. Running submitted code directly on the API server, unsandboxed, is dangerous mainly because…

    Untrusted code with no isolation can compromise the host, other users' data, or the network — the core risk of this entire system.

  2. Real sandboxing requires…

    A single isolation mechanism alone is insufficient — defense in depth layers several guarantees together.

  3. Submissions are queued and executed asynchronously (not synchronously in the API request) because…

    A durable queue keeps the API fast and available even during a massive contest-start submission burst.

  4. Time limits are enforced on CPU time (via cgroups) rather than wall-clock time because…

    CPU-time limiting keeps a submission's verdict consistent regardless of how busy the judge is at that moment.

  5. Fair per-user queueing during a contest exists to…

    Global throughput and per-user fairness are different goals; fair queueing balances both rather than optimizing throughput alone.

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.

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.

The qualities that shape everything

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 submission over 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 teardown over 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 queue over 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 limits over 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 queueing over 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.

The answer, out loud

What a strong answer to “Design an Online Judge” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    What this system actually is

    Let me scope it before I draw anything. An online judge takes code from strangers, runs it, and puts a verdict next to their name — and some of those strangers are actively trying to break the thing running them. So the property I’d put above everything else is that executing untrusted code never becomes a security or a fairness incident. Everything else sits underneath that. I’ll build in the order the constraints force: intake, isolated execution, judging against hidden tests, resource limits, then queueing — each piece only makes sense once the one before it holds.

  2. 3–8 min

    Why the naive version is disqualified

    I’ll start from the naive version, because it’s worth saying why it’s out rather than skipping past it. The API receives a submission and runs it — in process, or as a subprocess on the same box. The problem isn’t that it’s slow. It’s a full arbitrary-code-execution hole on infrastructure that’s serving every other user: whatever that process can read, the submission can read, and wherever the network reaches, it reaches. In most systems security is one requirement among several. Here, running hostile code safely is the product, so this is where the design genuinely starts.

    Built in step 1: Run the code right here
  3. 8–15 min

    What a sandbox has to guarantee

    So execution moves into a sandbox, and I want to be specific, because “we’ll put it in a container” isn’t an answer. A container with defaults still shares the host kernel, caps no CPU and no memory, can open sockets, and can fork until the host’s process table is gone. What I need is several guarantees holding at once: hard CPU and memory limits enforced through cgroups, no network at all, a filesystem cut down to what running the program needs, and a ceiling on processes and threads. And the environment is ephemeral — built for one run and destroyed after it. Layered, so that a gap in any one of them is still contained by the others.

    Built in step 2: A container alone isn't enough
  4. 15–21 min

    Submission and judging are different moments

    Now scale. Execution takes real time, and a contest opening means thousands of submissions inside a few seconds. If the request blocks until judging finishes, the API is holding thousands of connections open against multi-second runs and becomes the bottleneck at the worst possible moment. So the API accepts the submission, writes a job to a durable queue, and returns immediately. A pool of sandbox workers pulls from that queue and executes. Submission rate and judging capacity are now independent — workers scale to the burst, and the fast path never waits on the slow one.

    Built in step 3: Submission and execution are two different moments
  5. 21–27 min

    How a verdict is decided

    The verdict itself comes from running the submission against hidden test cases and comparing output against expected. Exact match, where there’s exactly one right answer. But plenty of problems have more than one valid output — a floating-point result within tolerance, several valid orderings — and a literal diff would reject perfectly correct submissions. For those, the problem ships a checker program that encodes the real correctness criterion instead of the expected text. And the judge stops at the first failing test, reporting that specific verdict — wrong answer, time limit, memory limit, runtime error — rather than burning worker time finishing a submission that already failed.

    Built in step 4: Hidden tests, checkers, and short-circuiting
  6. 27–33 min

    Limit CPU time, not the clock

    Then the time limit, which is where fairness gets interesting. The obvious implementation is a wall-clock timeout, and it’s wrong. Under contest load, workers contend for the same CPUs, so wall-clock time includes however long the process sat waiting for a slice it didn’t get. The same submission passes at midnight and fails at contest start, for reasons that have nothing to do with the code. So I’d limit the CPU time actually consumed, accounted through cgroups, alongside a real memory limit. Then the verdict reflects the computational work the algorithm did, which is the only thing a contest was ever trying to measure.

    Built in step 5: CPU time, not wall-clock
  7. 33–38 min

    Why the same code must get the same answer

    That also buys most of determinism, which is what a judge lives or dies on. If identical code is accepted once and gets a flaky time-limit verdict the next time, nobody trusts the standings. Host contention was the dominant source of that flakiness, and CPU-time limiting removes it. Fresh, thrown-away sandboxes remove the carried-over state. And I’d expose an explicit rejudge, so a disputed submission is rerun under controlled conditions instead of argued about in a forum thread. Consistency is something you engineer by removing causes one at a time — it isn’t a property you can hope a distributed system happens to have.

    Built in step 6: The same submission should get the same verdict, always
  8. 38–43 min

    Fair share under contest load

    The last piece is fairness in the queue. Strict global first-come-first-served sounds like the fair rule and isn’t: someone retrying a dozen near-identical attempts consumes near-term worker capacity belonging to people who submitted once and are waiting. So each user gets a bounded share of judging throughput — a cap on how many of their submissions are being judged at once, or a fair-share discipline over the queue — and worker autoscaling absorbs the aggregate spike. Contests are bursty but they’re also scheduled, so that scaling can be warmed ahead of a known start time rather than reacting after everyone has already arrived.

    Built in step 7: One user shouldn't starve everyone else
  9. 43–45 min

    Where I’d spend more time

    To close it: never execute inline, layered and ephemeral sandboxes, a durable queue between accepting and judging, hidden tests with checkers where a diff won’t do, CPU-time limits, and a per-user fair share. The trade-off running through all of that is that I’m spending throughput and some latency to buy safety and consistency — every one of those choices is slower than the naive version, and for a system whose verdicts carry real stakes I think that’s the right side of the trade. With more time I’d go at interactive problems, where the judge talks to the submission turn by turn instead of diffing its output, and at catching solutions copied between competitors — neither of those fits the one-shot run-and-compare model I’ve described.

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
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
NextDesign a Load Balancer Closest system design to this one

Explore the topic

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

More System Designs