Design problems
Platform & infrastructureMediumSDE-2 30 min

Design an online judge (LeetCode)

The interviewer asks

“Design an online coding judge like LeetCode or CodeChef. Users read a problem, write code in one of 15 languages, and submit; the system compiles and runs it against hidden test cases and returns Accepted, Wrong Answer, Time Limit Exceeded and so on. It also runs weekly contests with 100,000 participants and a live leaderboard.”

What a strong answer looks like

A strong answer recognises that this is two problems: running untrusted code safely, and handling very spiky load fairly. Submissions are stored, put on a queue and processed by a fleet of judge workers; the API returns a submission id immediately and the client gets the verdict over WebSocket or polling. Each worker runs code inside a strong sandbox (gVisor or Firecracker microVM, or nsjail with seccomp) with no network, cgroup limits on CPU time, memory, processes and output size, and a fresh filesystem per run. Test cases are versioned blobs cached on workers. Contests get their own queue with reserved and pre-scaled capacity so a contest's last-10-minute rush is not stuck behind practice traffic, and the leaderboard is a Redis sorted set with a composite score. Timing fairness (CPU time, pinned cores, rejudge on noisy results) and abuse (fork bombs, crypto mining, reading test files) are what separate a senior answer.

Concepts to mention

Async submission with queue and verdict pushStrong sandbox: gVisor/Firecracker/nsjail, seccomp, namespacescgroup limits for CPU, memory, pids and outputNo network and minimal filesystem in the sandboxCPU-time measurement, core pinning, rejudge for fairnessSeparate priority queues and reserved capacity for contestsScheduled pre-scaling plus queue-depth autoscalingVersioned, cached test dataCustom checkers and tolerant comparatorsRedis sorted set leaderboard with composite score

1 Requirements

Functional

  • Browse problems; submit code in ~15 languages (C++, Java, Python, Go, JavaScript, Rust...)
  • 'Run' against sample or custom input, and 'Submit' against all hidden test cases
  • Verdicts: Accepted, Wrong Answer, Time Limit Exceeded, Memory Limit Exceeded, Runtime Error, Compilation Error, with runtime and memory stats
  • Problems with multiple valid outputs use a custom checker program
  • Timed contests with a live leaderboard (score, penalty time, rank)
  • Submission history per user and per problem

Non-functional

  • Untrusted code must never escape the sandbox, reach the network or read test data it should not
  • Verdict p95 under 5 s in practice mode, under 15 s at contest peak
  • Contest scale: 100k participants, peak ~1k submissions/s in the last minutes; practice: ~5M submissions/day
  • Fair, repeatable timing: the same solution should not flip between AC and TLE
  • Leaderboard updates within ~5 s of a verdict
  • Submission data never lost once acknowledged

Out of scope (say it out loud)

  • Discussion forums and editorials
  • In-browser IDE features beyond a code editor
  • Interview video calls

2 Estimates

Practice load~60 submissions/s average, ~200/s peak5M/day ÷ 86,400 ≈ 58/s; 'Run' calls add ~2× more but use only sample tests
Contest load~200/s average, ~1k/s peak100k users × ~10 submits+runs over 90 min = 1M ÷ 5,400 s ≈ 185/s; first 10 and last 10 minutes ~5×
CPU per submission~2-4 CPU-secondsCompile ~0.5-1.5 s (C++/Java/Rust) + 50-100 test cases × ~20 ms average, with a few heavy cases near the time limit
Judge cores at contest peak~3,000-4,000 cores1k submissions/s × ~3 CPU-s = 3,000 busy cores; keep ~25% headroom so queue wait stays low
Test data~200 GB total, ~2 GB per contest3k problems × ~50 MB of test files (some very large inputs); cached on worker local SSDs by content hash
Submission storage~10 GB/day5M × ~2 KB code + metadata; code in S3 or Postgres TOAST, metadata in the DB

3 API

EndpointWhat it does
POST/v1/submissions{problem_id, language, code, contest_id?, mode: run|submit, custom_input?}; returns 202 {submission_id}
GET/v1/submissions/{id}Status and verdict with per-test stats (hidden test details masked)
WS/v1/streamPush verdict updates and contest announcements to the client
GET/v1/contests/{id}/leaderboard?page=&around=meRanked page or the window around the user's rank
GET/v1/problems/{slug}Statement, samples, limits, languages (cached on CDN)
POST/admin/problems/{id}/rejudgeRe-run all submissions after a test data fix

4 Data model

problemsPostgres
problem_idslugstatement_mdtime_limit_msmemory_limit_mbchecker_typetestset_versiontest_manifest (hashes)

Small, heavily cached. Statements served via CDN.

submissionsPostgres (partitioned by month) or Cassandra
submission_iduser_idproblem_idcontest_idlanguagecode_refstatusverdicttime_msmemory_kbfailed_testcreated_atjudged_at

Indexes on (user_id, created_at) and (contest_id, problem_id). Code itself in S3 for large volume.

test casesS3 + worker local SSD cache
problem_id/version/{n}.in.outchecker binary

Immutable, content-addressed; workers download once per version. Never mounted inside the sandbox except the current input.

judge queuesKafka / RabbitMQ / SQS
submission_idlanguageprioritycontest_id

Separate queues: contest, practice-submit, practice-run. Visibility timeout so a dead worker's job is retried.

leaderboardRedis sorted set
key contest:{id}; member user_id; score = solved × 10^9 − penalty_seconds

ZADD on each new AC; ZREVRANK for own rank; per-problem first-AC times in a hash. Final standings persisted to Postgres.

contest_scoresPostgres
contest_iduser_idproblem_idfirst_ac_atwrong_attempts

Source of truth to rebuild the sorted set and for final results.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAPOST submissionstore pendingenqueuetest data (cached)compile + runverdictZADD scoreverdictBrowserCDN + LBSubmission APIContest + leaderboardsvcWebSocket gatewayJudge queues (bypriority)Sandbox(gVisor/nsjail)Verdict eventsJudge workersPostgresRedis leaderboardS3 code + test data
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Submit a C++ solution

  1. 1Client POSTs code; API checks size (≤64 KB), rate limit (e.g. 1 submit per 5 s per user) and stores the submission as pending
  2. 2API enqueues {submission_id} on the contest or practice queue and returns 202 with the id
  3. 3A judge worker for C++ pulls the job, fetches code, ensures the problem's test set version is in its local cache (download from S3 if not)
  4. 4It starts a fresh sandbox from a warm pool, compiles with fixed flags (-O2, time limit 10 s for compilation), and fails fast with Compilation Error if needed
  5. 5It runs the binary on each test with cgroup limits (CPU time, memory, pids ≤ 64, output ≤ 64 MB), comparing output with a token-based comparator or the problem's checker
  6. 6It stops at the first failing test (for Submit) and records verdict, max time and memory
  7. 7Worker publishes the verdict event, updates the submission row, and destroys the sandbox; the WebSocket gateway pushes the verdict to the user

Accepted during a contest

  1. 1Contest service consumes the AC verdict; if it is the user's first AC on that problem, records first_ac_at and wrong attempts in Postgres
  2. 2It recomputes score = solved × 10^9 − (sum of solve times + 5 min × wrong attempts) and ZADDs it to contest:{id}
  3. 3Leaderboard pages are read from Redis with a 2-5 s cache for the top pages; users get their own rank with ZREVRANK

Test data bug found mid-contest

  1. 1Setter uploads a corrected test set; a new testset_version is created (old files stay immutable)
  2. 2Admin triggers rejudge of affected submissions on a low-priority queue
  3. 3Changed verdicts produce events that update contest scores and the leaderboard

7 Deep dives

Sandboxing untrusted code

Every submission is hostile until proven otherwise: fork bombs, infinite memory allocation, reading /etc or test files, opening sockets to mine crypto or exfiltrate data, or exploiting the kernel. Plain Docker containers share the host kernel and are not a security boundary on their own. Use defence in depth. Isolation: gVisor (a user-space kernel that intercepts syscalls) or Firecracker microVMs (lightweight VMs with ~125 ms boot) give a much smaller attack surface; nsjail or isolate (used by many contest systems) combine Linux namespaces, seccomp-bpf syscall filters and cgroups. Limits via cgroups v2: CPU time, memory (memory.max, then MLE), pids.max to stop fork bombs, file size and output size. No network namespace interfaces at all. Filesystem: read-only base image per language, a small tmpfs as the working directory, only the current test's input mounted; expected outputs and other tests stay outside the sandbox. Run as an unprivileged user with no capabilities. Destroy the sandbox after each submission. Finally, run judge hosts in an isolated network segment with no credentials on the machine, so even a full escape finds little to steal.

Fair and repeatable timing

Users notice when the same code gets TLE once and AC the next time, and in contests it changes rankings. Sources of noise: other jobs on the same machine competing for CPU cache and memory bandwidth, CPU frequency scaling, and wall-clock delays from I/O. Mitigations: measure CPU time (user + sys from cgroup accounting), not wall time, for the limit, but also enforce a wall-clock limit (2-3× CPU limit) so sleeping programs cannot hang a worker. Pin each sandbox to dedicated physical cores (cpuset) and run fewer concurrent sandboxes than cores, avoiding hyperthread siblings. Use a homogeneous instance type for the contest pool and disable turbo or fix frequency where possible. Language multipliers (Java and Python get more time) or per-language limits keep it fair across languages. Run each test once normally; if the total is within ~10% of the limit, re-run the slow tests and take the minimum, which removes most flaky TLEs for a small extra cost. Publish the judge hardware and compiler versions so users can reason about limits.

Contest spikes and queue design

Practice traffic is steady; contests concentrate tens of thousands of users into the same 90 minutes, with peaks at the start (easy problem) and the last minutes. One shared queue would let a contest flood practice users, or worse, let practice traffic delay contest verdicts. Use separate queues: contest, practice-submit and practice-run, with workers that always pull from contest first and with a reserved worker pool for contests. Because contests are scheduled, scale before they start: a scheduled job raises the contest pool to the expected size 15 minutes early (machines need minutes to boot and warm caches), then queue-depth autoscaling (KEDA on queue length) handles the rest. Pre-load test data for that contest's problems onto workers before start. Warm sandbox pools per language avoid ~1 s startup per job. Under extreme load, degrade gracefully: 'Run' with custom input is rate-limited first, then practice is paused, and contest submissions keep flowing. Show queue position to users; transparency reduces resubmits that make the queue worse.

Judging logic: comparators, checkers and early exit

Output comparison is not a simple byte compare. Default comparators are token-based (ignore trailing whitespace and line endings) because Windows and Linux line endings otherwise cause angry Wrong Answers. Floating-point answers use an absolute or relative error tolerance such as 1e-6. Problems with many valid answers (print any shortest path) ship a checker program that reads input, expected output and the user's output and decides; checkers run in the sandbox too, with their own limits, since setters make mistakes. Interactive problems need a judge process talking to the user's program over pipes, with both limited. For speed, Submit stops at the first failing test, and tests are ordered with samples and small cases first so most wrong solutions fail in milliseconds. Large inputs (100 MB) stay on local SSD and are passed by file descriptor, not copied. LeetCode-style function problems wrap the user's function in a per-language harness that parses input and serialises output, which must be generated and tested per language.

8 Trade-offs

gVisor / Firecracker vs plain containers with seccomp

MicroVMs and gVisor are safer but add overhead (gVisor slows syscall-heavy programs; Firecracker adds boot time, mitigated with warm pools and snapshots). nsjail-style sandboxes are fast and proven in contest systems. For a public site where anyone can submit, pay for the stronger boundary.

Async verdict vs synchronous API

Holding the HTTP request open for 5-15 s ties up connections and breaks on retries. Async with WebSocket push (polling as fallback) is more robust and lets the queue absorb spikes.

Redis sorted set vs DB queries for the leaderboard

Redis gives O(log n) updates and rank lookups for 100k users easily. Postgres stays the source of truth so the board can be rebuilt; final standings are frozen from Postgres.

Own fleet vs serverless execution

Lambda-style functions scale instantly but have cold starts, inconsistent CPU and per-invocation cost that hurts at contest scale. A dedicated, homogeneous fleet with scheduled scaling gives fairer timing; use spot instances only for practice runs.

9 Bottlenecks

Judge capacity in the last 10 minutes

3-4k cores needed for a short time. Pre-scale, reserve on-demand capacity for contest pools, and keep practice on spot instances that can be preempted.

Leaderboard reads

100k users refreshing the board. Cache top pages for a few seconds, serve 'around me' with ZREVRANK + ZREVRANGE, and freeze the board in the last 15 minutes like many contests do, which also reduces load.

Test data downloads

Hundreds of new workers pulling 2 GB each at contest start can saturate S3 bandwidth for one prefix. Pre-warm caches before the contest and share via a node-local cache.

10 Failure handling

Worker dies mid-judging

The job's visibility timeout expires and another worker re-runs it. Judging is idempotent: same code, same tests, same verdict written to the same submission row.

Sandbox escape attempt detected

Seccomp violations kill the process with Runtime Error and log a security event; repeated attempts suspend the account. Host-level anomaly detection drains and reimages the machine.

Redis leaderboard lost

Rebuild the sorted set from contest_scores in Postgres in seconds (100k rows). Users see the board briefly unavailable, never wrong final results.

11 What impresses

  • Stating that containers alone are not a security boundary and layering defences
  • Explaining CPU time vs wall time and noisy-neighbour effects on verdicts
  • Pre-scaling for scheduled contests rather than relying only on reactive autoscaling
  • Keeping expected outputs outside the sandbox
  • Graceful degradation order under overload (custom runs, then practice)
  • Encoding rank as one composite sorted-set score

12 Mistakes

  • Running user code directly on the API server or in a plain container with network access
  • Measuring wall-clock time only, leading to flaky TLEs
  • One queue for contests and practice
  • Synchronous judging inside the HTTP request
  • Mounting all test cases (including outputs) into the sandbox
  • Recomputing the leaderboard with SQL ORDER BY on every request

13 Follow-ups

Q1How would you detect plagiarism after a contest?
Q2How would you support a new language within a day?
Q3How do you stop someone from using the judge as free compute for crypto mining?
Q4How would you add AI-generated hints without leaking the solution?
Q5How do you handle a contest where the judge falls 20 minutes behind?
Q6How would you run this for a company hiring test with webcam proctoring?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free