Home › Blog › Design an Online Judge

Design an Online Judge (LeetCode-Style) System

How to design a LeetCode-style judge in a system design interview: the submission pipeline, sandboxed execution, test data, contest-day load and live leaderboards.

An online judge takes a user's code, compiles and runs it against hidden test cases inside an isolated sandbox, and returns a verdict such as Accepted, Wrong Answer or Time Limit Exceeded. The interview-ready design is an API that records each submission and puts a job on a queue, language-specific workers that run the code in locked-down containers or microVMs with hard CPU, memory, process and network limits, and a results endpoint the client polls. The depth interviewers probe is sandbox security, test data delivery, and surviving a contest where tens of thousands of people submit at once.

Clarify the requirements first

Published walkthroughs pick different numbers; Hello Interview's breakdown of this question, for example, targets results within 5 seconds and a contest with 100,000 users. Ask rather than assume: how many languages, whether “Run code” is in scope, whether contests give partial credit per test, and whether plagiarism detection is in scope (usually not; mention it as a later batch job).

Quick capacity math

Suppose 10,000 submissions land in the same minute, each with about 100 test cases at a pessimistic 100 ms per test:

10,000 submissions × 100 tests × 0.1 s = 100,000 CPU-seconds
Finish within 60 s  →  100,000 / 60 ≈ 1,667 cores busy for that minute

Real numbers are lower, since many tests finish in tens of milliseconds and a wrong answer can stop at its first failing test, but no single machine covers that order of magnitude. A queue and a horizontally scaled worker fleet are not optional.

Core entities and API

EntityKey fieldsWhere it lives
Problemstatement, time and memory limits, code stubs, current test-set versionPrimary database, cached
Test setinputs, expected outputs, comparison modeObject storage, immutable per version
Submissionuser, problem, contest, language, code, status, verdict, runtime, memory, test-set version, created_atPrimary database, indexed by user and contest
Leaderboardcontest, user, solved count, penalty timeRedis sorted set, rebuildable from submissions
POST /problems/{id}/submissions  {language, code}         → 202 {submissionId}
GET  /submissions/{id}                                     → {status, verdict, runtimeMs, memoryKb}
POST /problems/{id}/run          {language, code, stdin}  → 202 {runId}
GET  /contests/{id}/leaderboard?page=1

Take the user ID from the session token, never the request body, and stamp the submission time on the server. In a contest, a client-supplied timestamp is an invitation to game the ranking.

The submission pipeline, end to end

  1. Accept. Authenticate, apply a per-user rate limit (see Design a Rate Limiter), reject oversized code, and check the language and contest window.
  2. Record and enqueue. Write a submission row with status QUEUED and the server timestamp, publish {submissionId, language, problemId, testSetVersion} to that language's queue, and return 202 with the ID.
  3. Claim. A worker in the matching pool pulls the message, and no other worker can claim it until it is acknowledged or its lease expires. A crashed worker means a retry, not a lost submission.
  4. Compile and run. Compile inside a sandbox with its own limits, then run each test case in a fresh sandbox and compare output as you go.
  5. Report. Write the verdict, runtime, memory and first failing test, emit a “judged” event for the leaderboard, and acknowledge the message.
  6. Deliver. The client polls GET /submissions/{id} about once a second until the status is final, or gets a push over WebSocket or server-sent events.

Cover two failure cases before you are asked. If the row is written but the enqueue fails, the submission sits in QUEUED forever; use a transactional outbox or a sweeper that re-enqueues stale rows. If a worker dies mid-judging, the job runs again, so judging must be idempotent, with the leaderboard update keyed on the submission ID. The lease, retry and dead-letter mechanics match Design a Distributed Job Scheduler. And keep judge failures separate from user failures: a missing test file or sandbox setup error is an internal error to retry, never a Runtime Error shown to the user.

Sandboxed execution: the deep dive

You are running arbitrary code from strangers, and some of it will try to read your files, open network connections, fork until the machine falls over, or loop forever. Four common isolation levels:

OptionHow it isolatesTrade-off
Process sandbox (e.g. isolate)Linux namespaces, cgroups and per-process limits around one runVery fast setup, built for untrusted contest programs; shares the host kernel
Hardened containers (Docker + seccomp)Namespaces, cgroups, syscall filter, dropped capabilitiesFamiliar tooling and one image per language; shared kernel, easy to misconfigure
User-space kernel (gVisor)Intercepts system calls and handles them in its own application kernelMuch smaller host-kernel exposure; slower for syscall-heavy code, not every syscall supported
MicroVMs (Firecracker)A KVM-backed virtual machine per workloadVM-level boundary; the project cites boot under 125 ms and under 5 MiB overhead; more to operate

A defensible answer: per-language container pools for speed, each run inside a fresh, tightly limited sandbox, with gVisor or Firecracker as the upgrade when the threat model demands more than a shared kernel. Name full VMs (too slow to start) and serverless functions (cold starts, execution caps) as rejected alternatives, with the reason.

Limits to enforce on every run

The code around the sandbox is attack surface too. In 2024, Tanto Security disclosed sandbox escapes in Judge0, an open-source code execution system (CVE-2024-28185, CVE-2024-28189, CVE-2024-29021). Contributing causes included running the container with --privileged, running the application as root, and host-side code following a symlink a submission planted in its sandbox directory. Fixes shipped in 1.13.1. The lesson: never run judge containers privileged, run the orchestrator unprivileged, and treat every file a submission leaves behind as hostile.

Keep a warm pool of containers per language so no submission waits on an image pull or runtime boot, but create a fresh sandbox (or reset the scratch filesystem) for every run so one user's state never leaks into the next.

Test case storage and verdicts

VerdictTypical trigger
AcceptedEvery test passed within the limits
Wrong AnswerOutput did not match, or the checker rejected it
Time Limit ExceededCPU or wall-clock limit hit
Memory Limit Exceededcgroup memory cap hit
Runtime ErrorCrash, uncaught exception or non-zero exit
Compilation ErrorCompile step failed; return the compiler message
Internal ErrorThe judge failed; retry and never blame the user

For fair timing, run judges on one consistent machine type, do not run more concurrent jobs than physical cores, and set limits with headroom over the reference solution. Whether slower languages get a time multiplier varies by platform, so state your choice.

Contests at peak load

A contest is a scheduled stampede: everyone opens the same problems in the same second, submits in waves, and rushes again at the end. Because the start time is known, prepare instead of react:

The same everyone-at-once pattern drives Design Ticketmaster, where a ticket drop plays the role of the contest start.

Results delivery and live leaderboards

For individual results, short polling is simple and usually enough; Hello Interview's breakdown says LeetCode itself polls for submission results. WebSockets or server-sent events cut wasted requests but add connection state, so present them as an upgrade.

For the leaderboard, the submissions table stays the source of truth and a Redis sorted set per contest serves reads. A common rule ranks by problems solved, then by lower penalty time (time to each accepted solution, sometimes plus a charge per rejected attempt, depending on the rules), so pack both into one score:

score = solved × 10,000,000 − penalty_seconds

on first Accepted for (user, problem):
  ZADD contest:{id}:board {score} {user_id}

top 100:  ZRANGE contest:{id}:board 0 99 REV WITHSCORES
my rank:  ZREVRANK contest:{id}:board {user_id}

Award a problem only on the first accepted submission, rebuild the set from the database if it is lost, and if test data proves wrong mid-contest, rejudge affected submissions at low priority and recompute. Design a Leaderboard covers the ranking structure in more depth.

Trade-offs to say out loud

DecisionOptionsReasonable lean
Execution modelSynchronous call vs queue plus workersQueue: survives spikes and worker crashes
IsolationHardened containers vs gVisor or microVMsContainers with strict limits; stronger boundary if the threat model needs it
Result deliveryPolling vs WebSocket or SSEPolling first; push if request volume demands it
Runtime imagesOne image for all languages vs one pool per languagePer language: smaller images, independent scaling
JudgingStop at first failure vs run every testStop early for pass/fail; run all for partial credit

Pacing for 45 minutes: about 5 minutes on requirements, 5 on entities, API and capacity math, 10 on the end-to-end pipeline, 15 on deep dives (sandbox first, then contests or the leaderboard), and the rest on failure modes and trade-offs.

Hello Interview's breakdown spells out level expectations: mid-level candidates should land the API, data model and the need for isolation, while senior candidates move quickly through the high-level design, compare containers, VMs and serverless functions, and explain how test cases actually run against user code. The sandbox comparison is easy to know and harder to say crisply against a clock, so rehearse it aloud; a free AI mock interview in CoPilot Interview can include system design questions and scores each spoken answer.

Rehearse this design out loud

Paste a job description and your résumé into CoPilot Interview, set the question mix to include system design, and an AI interviewer reads each question aloud, listens to your answer and scores it with what worked and what to improve. The free plan gives you 15 minutes a day with no credit card.

Try a free AI mock interview

FAQ

What is an online judge system?

An online judge is a service that accepts code submissions, compiles and runs them against hidden test cases inside an isolated sandbox, and returns a verdict such as Accepted, Wrong Answer or Time Limit Exceeded. LeetCode-style practice sites and programming contests are built around one.

Should an online judge run code in containers or virtual machines?

Hardened containers with strict CPU, memory, process and network limits are the usual interview answer because they start fast and are cheap to pool per language. If the threat model needs a stronger boundary than a shared kernel, add a user-space kernel such as gVisor or use microVMs such as Firecracker.

How does an online judge stop infinite loops and fork bombs?

Each run gets a CPU-time limit and a separate wall-clock limit, so busy loops and sleeping programs are both killed. A process limit, such as allowing a single process or setting a Docker pids limit, stops fork bombs before they exhaust the machine.

Why put a queue between the API and the code execution workers?

A queue turns a contest spike into a short wait instead of errors, lets each language pool scale independently, and keeps a submission safe if a worker crashes, because the message is redelivered when its lease expires. The client then polls for the result or receives a push once judging finishes.

How do you build a live leaderboard for a coding contest?

Keep submissions in the database as the source of truth and maintain a Redis sorted set per contest, with a score that packs problems solved and penalty time into one number. Update it idempotently on each first accepted submission and serve the top pages from a short-lived cache.