← System design

Infrastructure

Design LeetCode

An online judge is a remote code execution service that you deliberately expose to strangers. Every request is a program that might loop forever, allocate until the kernel panics or try to read the answer files, and yet the person who sent it expects a correct verdict in a few seconds, measured to the millisecond against everyone else in the contest. The design is about running hostile code cheaply, fairly and at a bursty scale.
Core: isolated runners fed by a priority queue6M submissions a dayUntrusted code, zero host accessContest verdict p95 under 8 s

01

What the judge must do

02

What the judge must do

Functional
  • A user picks a problem and a language, pastes source code and submits it for judging against hidden test data.
  • The result is one verdict: Accepted, Wrong Answer, Time Limit Exceeded, Memory Limit Exceeded, Runtime Error or Compilation Error, plus runtime and peak memory.
  • A separate "try it" action runs the code on the visible examples or on custom input and shows stdout, with no effect on stats.
  • Timed contests with a live scoreboard ranked by solved count, then by penalty time.
  • Each problem has its own time and memory limits, and limits can be scaled per language (Python gets a multiplier).
  • Users can browse their submission history and reopen any old source file.
Non-Functional
  • Untrusted code must never reach the host, the network or another user's files.
  • Verdicts are reproducible: the same code on the same tests gives the same verdict and similar timing, run after run.
  • During contests, p95 time from submit to verdict stays under 8 seconds.
  • No submission is lost once the API has returned an ID, even if a runner dies mid-execution.
  • Hidden test data is never exposed through output, errors or timing side channels that leak a whole file.
Clarify first

Ask how big the largest contest is and how quickly people submit after it opens, because that burst sizes the whole runner fleet. Ask whether interactive problems (where the program talks to a grader) and special checkers (problems with many correct outputs) are in scope. Finally, ask whether timing must be comparable across submissions. If yes, you need dedicated CPU cores per run, which changes the cost model more than anything else on this page.

02

Sizing the fleet

03

Sizing the fleet

Assume 6 million submissions on a normal day, two thirds of them "try it" runs on examples and one third full judgements. A weekly contest draws 60,000 people over 90 minutes. Each full judgement runs about 80 test files on average.

QuantityWorkingResult
Average submission rate6M / 86,400 sabout 70 / s
Full judgements per day6M x 1/32M
Contest submissions60K users x 7 attempts on averageabout 420K in 90 min
Contest peak ratefinal 10 minutes carry about 20% of attemptsabout 140 / s
CPU per full judgementcompile 1.5 s + 80 tests x 25 ms averageabout 3.5 core-seconds
Cores busy at contest peak140 / s x 3.5 core-s, plus normal trafficabout 700 cores
Runner hosts at peak700 / 28 usable cores, 30% headroomabout 33 hosts
Source storage per year6M x 365 x 2.5 KB, compressed 4:1about 1.4 TB
Test data4,000 problems x 50 MB averageabout 200 GB, cached on every host
Scoreboard updatesone per accepted verdictunder 50 / s
What the numbers say

Request volume is tiny by web standards. The cost is CPU seconds, and the shape of the load: a flat baseline with a cliff every Sunday. The fleet you need for the last ten minutes of a contest sits mostly idle the rest of the week, which is why autoscaling and a pre-warmed pool matter more than any database choice.

Test data fits on local disk

200 GB of inputs and expected outputs fits on one NVMe drive. Sync it to every runner host and read it locally instead of fetching from object storage for each submission.

03

API design

04

API design

POST/v1/problems/{slug}/judgementsQueue a full judgement
// Idempotency-Key header lets a flaky client retry without a double entry { "language": "cpp20", "source": "#include <bits/stdc++.h>\nint main(){...}", "contest": "weekly-412" // omit for practice } // 202 Accepted { "judgement_id": "jd_01J9Y4QH2M", "state": "queued", "events": "wss://judge.example.com/v1/judgements/jd_01J9Y4QH2M/events" } // 413 if the source exceeds 64 KB, 429 if the user has 3 runs in flight
GET/v1/judgements/{id}Poll a verdict
{ "state": "done", "verdict": "TIME_LIMIT", "failed_on": 57, // test index only, never its contents "passed": 56, "total": 92, "max_time_ms": 2004, "max_memory_kb": 48120 }
POST/v1/scratch-runsRun on examples or custom stdin
{ "problem": "two-sum", "language": "python3", "source": "...", "stdin": "4\n2 7 11 15\n9" } // Runs on the low priority lane; output truncated at 8 KB
GET/v1/contests/{id}/standings?offset=0&limit=50Scoreboard page
{ "frozen": false, "as_of": "2026-10-04T15:41:07Z", "rows": [ { "rank": 1, "user": "tourist_fan", "solved": 4, "penalty_s": 1830 } ] }
Asynchronous by design

Submitting returns 202 immediately and the verdict arrives over a WebSocket or by polling. Holding an HTTP request open for the duration of compilation and 90 test runs would tie up gateway connections exactly when the contest burst hits.

04

Architecture overview

05

Architecture overview

Submission path from API to verdictDiagram
flowchart LR
	U["Browser"] --> G["API gateway"]
	G --> SA["Submission API"]
	SA --> DB[("Postgres: submissions")]
	SA --> BS[("Object store: source files")]
	SA --> Q[["Priority queue: contest, judge, scratch"]]
	Q --> D["Runner agent on host"]
	D --> SB["Sandbox: compile then run"]
	TD[("Local test data cache")] --> SB
	SB --> D
	D --> V[["Verdict topic"]]
	V --> SA
	V --> SC["Scoreboard service"]
	SC --> RZ[("Redis sorted sets")]
	V --> WS["WebSocket push"]

The submission API is the only piece the browser talks to. It checks the language, size and rate limits, writes a row with state queued, puts the source file in object storage and pushes a small job record onto the queue. The job holds the IDs, the language, the problem version and the limits. It never holds the test data, and the source is fetched by the runner rather than packed into the message.

Runner hosts are where the risk lives, so they are kept boring. Each host runs one trusted agent that pulls jobs, prepares a sandbox, feeds it inputs from a local read-only copy of the test data, compares output and reports back. User code only ever exists inside the sandbox. The agent publishes a verdict event, and three consumers react: the submission API updates the row, the scoreboard service adjusts standings for contest jobs, and a push service forwards the result to the open WebSocket.

Three-lane queue

Contest jobs, full practice judgements and scratch runs live in separate lanes. Agents always drain the contest lane first and take scratch runs only when the others are empty, so the Sunday burst never waits behind someone testing a print statement.

Runner agent

A trusted daemon per host that owns a pool of idle sandboxes, pins each run to dedicated cores, enforces limits, and acknowledges the queue message only after the verdict is published. If it dies, the message reappears for another host.

Test data cache

Every problem version is a content-addressed bundle synced from object storage to each host's disk. Jobs reference the bundle hash, so a test fix made mid-contest cannot mix old and new files inside one judgement.

Scoreboard

One Redis sorted set per contest with a packed score of solved count and penalty. Ranks are read with ZREVRANK, and pages with a range query, so standings stay fast at 60,000 rows.

05

Deep dive: running hostile code fast and fairly

06

Deep dive: running hostile code fast and fairly

The core problem

You are running thousands of untrusted programs a minute and you owe each one an exact verdict. That needs two things that fight each other: strong isolation, which costs startup time, and fast turnaround, which punishes every millisecond of setup. This section is about getting both.

Layers of isolation

No single mechanism is enough, so stack them. The outer layer is the boundary between user code and the host kernel. Plain containers share that kernel, and one kernel bug turns into a host compromise. A user-space kernel such as gVisor intercepts system calls and answers most of them itself, which shrinks the exposed surface for a modest syscall overhead. A microVM such as Firecracker gives each run its own guest kernel, the strongest wall, with a boot in the low hundreds of milliseconds and a few megabytes of memory overhead.

Inside that wall, apply the usual Linux controls. A cgroup caps memory (the limit plus a small allowance for the runtime), process count (blocks fork bombs) and CPU. A seccomp profile allows only the syscalls a compiled program or interpreter needs. Network namespaces are empty, so there is simply no interface to talk to. The filesystem is a read-only image of the compiler toolchain plus a small tmpfs scratch directory capped at, say, 64 MB. The process runs as an unprivileged user with every capability dropped.

Finally, the agent watches from outside: a wall-clock timer kills the run at twice the time limit even if CPU time is low, which catches programs that sleep or block on input forever.

One judgement inside a runner hostDiagram
flowchart TD
	J["Job pulled from contest lane"] --> W["Take a warm sandbox for cpp20"]
	W --> C{"Compiles within 10 s?"}
	C -->|no| CE["Verdict: compilation error"]
	C -->|yes| T["Run next test with cgroup limits"]
	T --> K{"Exit, time and memory OK?"}
	K -->|no| BAD["Verdict: TLE, MLE or runtime error"]
	K -->|yes| CMP{"Output matches checker?"}
	CMP -->|no| WA["Verdict: wrong answer on test i"]
	CMP -->|yes| MORE{"More tests?"}
	MORE -->|yes| T
	MORE -->|no| AC["Verdict: accepted"]
	CE --> R["Destroy sandbox, refill pool"]
	BAD --> R
	WA --> R
	AC --> R

Warm pools hide the startup cost

If each job had to boot a microVM, mount an image and start a JVM, setup alone could exceed the run itself. So each agent keeps a pool of sandboxes already booted for the popular languages, sized from recent demand: perhaps eight C++ sandboxes, six Python, four Java, two of everything else. A job claims one, runs, and the sandbox is destroyed afterwards, never reused. Reuse would let one user leave a file or a background process for the next. The agent boots a replacement in the background. With Firecracker snapshots you can restore a pre-initialised VM in tens of milliseconds, which makes the pool cheap to refill.

Compilation happens inside the same sandbox as execution, with its own limits (10 seconds, 1 GB). Compilers have had bugs that hang or blow up on crafted input, and a template-heavy C++ file can be a denial of service on its own.

Measuring time so verdicts are fair

Contestants compare their runtimes, and a Time Limit verdict on a solution that passed yesterday is a support ticket. Three habits keep timing honest. Pin each run to dedicated physical cores with hyperthread siblings left idle, so a neighbour cannot steal cache or execution units. Measure CPU time from the cgroup, not wall-clock time, and use wall-clock only as the safety kill. And disable turbo frequency scaling on runner hosts, because a chip that boosts when cool and throttles when hot makes the same program vary by 20 percent.

Per-language multipliers are a policy choice, not physics. A common approach sets the limit for the reference C++ solution, then grants interpreted languages a fixed multiplier, and checks during problem setup that a reasonable Python solution passes.

Checking output

Most problems compare tokens after trimming whitespace. Floating point problems compare with a tolerance like 1e-6. Problems with many valid answers ship a checker program, written by the problem setter, that reads the input, the user output and the reference answer and returns a verdict. The checker runs in a sandbox too, with its own limits, because user output can be huge or malformed. Interactive problems swap the static input file for an interactor process connected to the user program by two pipes, both inside the same isolated environment.

For contests, stop at the first failing test. That saves compute and returns verdicts faster when the cluster is busiest. Practice judgements can run everything and report how many passed, which helps learners.

A strong interview answer

"Each run gets a fresh, never-reused sandbox: a microVM or gVisor boundary, then cgroups, seccomp, no network and a read-only filesystem. The agent keeps a warm pool per language to hide startup, pins each run to its own cores, and reports CPU time from the cgroup. Contest jobs drain first and fail fast. Test data is content-addressed and local, so every judgement uses exactly one version."

06

Mistakes to avoid

⚠

Mistakes to avoid

🚫
Reusing a sandbox between users to save boot time

A program can leave a background process, a file in /tmp or a modified environment behind. The next user inherits it, and their verdict, or their source code, is no longer private.

✓ Better: destroy every sandbox after one job and refill a warm pool from a clean snapshot.
🚫
Timing runs with wall-clock time on shared cores

A noisy neighbour or a garbage collector on the host adds hundreds of milliseconds, so correct solutions get Time Limit on busy days and contestants lose rating points.

✓ Better: pin runs to dedicated cores, read CPU time from the cgroup, and keep wall-clock only as a hard kill switch.
🚫
Putting the source and test data inside the queue message

Messages become megabytes, the broker slows during the burst, and test files end up copied into logs and dead letter queues where anyone with broker access can read them.

✓ Better: send IDs and a bundle hash; runners fetch the source from storage and read tests from a local cache.
🚫
Echoing the failing test's input back to the user

With a few dozen deliberate failures, a user can print the hidden inputs one by one and hardcode the answers.

✓ Better: return the test index and verdict only, except for public example tests.
🚫
Recomputing the scoreboard with a SQL query per page view

Sixty thousand contestants refresh standings every few seconds near the end, and a GROUP BY over every submission locks up the primary.

✓ Better: update a sorted set on each accepted verdict and serve pages from it.

07

Decisions and what they cost

07

Decisions and what they cost

Chosen
MicroVM or gVisor per run, from a warm pool

A kernel-level wall between user code and the host, at the price of some boot time and memory per sandbox. The warm pool turns that boot time into background work, and snapshots make refills quick.

Isolation you can defend in a security review
Rejected
Plain containers with seccomp only

Fastest to start and cheapest to run. But every run shares the host kernel, so one privilege escalation bug exposes the whole host, including other users' code and the test data. Acceptable for an internal tool, not for public traffic.

Too thin for untrusted code
Chosen for contests
Fail fast on the first bad test

Cuts CPU use by roughly half on wrong submissions and returns the verdict sooner. Contestants only need to know that they failed and roughly where.

Contest lane default
Chosen for practice
Run every test and report the count

"Passed 71 of 92" tells a learner whether the idea is close or wrong. It costs more compute, which is fine on the practice lane where nobody is racing a clock.

Practice only
Chosen
Dedicated cores with frequency scaling off

Stable, comparable timing. The cost is lower packing density: you leave hyperthread siblings idle and run fewer jobs per host than the raw core count suggests.

Fairness is the product
Alternative
Instruction counting instead of time

Counting instructions with hardware counters is perfectly deterministic and lets you pack hosts densely. It also ignores cache misses and memory bandwidth, so it can reward code that would be slow on real hardware. Some judges use it; most still prefer CPU time.

Worth mentioning
Chosen
Queue with priority lanes plus autoscaling

A durable queue absorbs the burst and lanes protect contest latency. Runner hosts scale up ahead of each scheduled contest because start times are known days in advance.

Predictable bursts, planned capacity
Rejected
Serverless functions per submission

Attractive for spiky load, but most function platforms limit syscalls, cold starts are unpredictable for heavy runtimes, and you lose control over core pinning, which wrecks timing fairness.

Timing becomes noise

08

Failure modes

08

Failure modes

🔓
A submission tries to break out of the sandbox

Someone submits code that probes a kernel exploit. With plain containers, success means root on a runner that also caches every problem's hidden answers.

Keep a kernel boundary (microVM or gVisor) between code and host, run hosts with no credentials beyond reading test bundles, rebuild hosts from images regularly, and alert on any denied syscall pattern that looks like probing.
⏳
The contest queue backs up in the last ten minutes

Submissions arrive faster than runners finish. Verdicts that normally take 3 seconds take 90, and contestants resubmit, which makes the backlog worse.

Scale runner hosts up an hour before the start, pause the scratch lane during the contest, cap in-flight jobs per user, and record queue time separately so penalty is based on submit time, not verdict time.
🎲
Same code, different verdict

A solution near the time limit passes on one host and fails on another. Contestants post side by side screenshots and trust in the ranking drops.

Run identical hardware in the contest pool, pin cores, disable turbo, and automatically re-run any Time Limit verdict within 10 percent of the limit on a second host before publishing it.
💣
Fork bombs and output floods

A program spawns processes in a loop or prints gigabytes to stdout. Without limits it starves the host, and the agent tries to buffer all that output in memory.

A pids limit in the cgroup stops the fork bomb, and output is read through a capped pipe: past the limit, the run ends with an Output Limit verdict.
🧩
A broken test file is found mid-contest

A setter discovers that test 33 has a wrong expected answer. Hundreds of people have been marked wrong.

Publish a fixed bundle under a new hash, then rejudge every affected submission against it from a dedicated rejudge lane, and recompute standings from the new verdicts.
🕵️
Copied solutions during a live contest

A solution leaks to a chat group and dozens of accounts submit near-identical code within minutes.

Run similarity checks on normalised token streams or syntax trees after the contest, flag clusters for human review, and hold rating changes until review ends.

09

How to handle the interview

09

How to handle the interview

1

Lead with the threat model. Say out loud that every request is hostile code and list what it might try: loop forever, exhaust memory, fork, open sockets, read the test files. Then map each threat to a control.

2

Make submission asynchronous. Return an ID straight away and push the verdict later. Interviewers want to see you separate the API from the slow, risky work.

3

Size the fleet from CPU seconds, not requests. Seventy requests a second sounds small until you multiply by compile and test time and apply the contest burst.

4

Explain how you keep timing fair. Dedicated cores, CPU time from cgroups and a re-run near the limit show you understand that the verdict is the product.

5

Name the priority lanes. Contest, practice and scratch runs have different stakes. Draw them, and explain what you throttle when the cluster is full.

6

Version the test data. Content-addressed bundles make rejudging clean and stop a mid-contest fix from mixing files inside one run.

7

Treat the scoreboard as its own small system. A sorted set updated by verdict events, plus a freeze in the final hour if the product wants suspense.

8

Mention what you return on failure. Only the test index for hidden tests, never the input, or you have built a test data download service.

10

Growing the design in stages

11

Growing the design in stages

Stage 1: a few hundred submissions a day
One box and a job table

A web app writes jobs to a Postgres table and a worker process on the same machine runs each one in a locked-down container. Good enough for a university course, as long as nobody hostile shows up.

Stage 2: tens of thousands a day
Separate runner hosts and a real queue

Runners move to their own machines with no access to the main database. A message queue replaces the job table, and verdicts flow back as events.

Stage 3: public contests
Kernel isolation, lanes and timing discipline

Contests bring motivated attackers and rating disputes. Add microVM or gVisor sandboxes, priority lanes, core pinning and versioned test bundles.

Stage 4: millions a day
Warm pools and planned scaling

Startup cost dominates, so agents keep warm sandboxes per language restored from snapshots. Capacity scales up ahead of each scheduled contest, and the single shared worker pool is gone.

Stage 5: global users
Regional runners and integrity tooling

Runner pools in several regions cut latency for scratch runs, while contest judging stays on one identical hardware pool for fairness. Plagiarism detection and automatic rejudging become standard tooling.

Watch and read

Try next

Free to read · better with landed

Whiteboard this with landed

landed runs it as a live system design round on the whiteboard and grades your trade-offs.