Skip to main content
PProfileMatch

MediumProblem 9 of 36 in the system design series

Design LeetCode: An Online Judge for Untrusted Code and Contests

Prefer watching? This article is the written companion to the video above, from the Shubham Aher YouTube channel.

An online judge lets people pick a programming problem, write a solution in the language they prefer, and receive a verdict a few seconds later telling them whether the code passed every hidden test. From the outside it resembles a content site with an editor attached, which is exactly why it makes a good interview question: the interesting engineering is invisible to the user.

Three things make a LeetCode-like service hard. First, every submission is arbitrary code written by a stranger, and the platform has to execute it without letting it escape, snoop or exhaust resources. Second, scheduled contests produce sharp, predictable bursts where thousands of people submit within the same minute. Third, contestants expect a ranking that updates within seconds of each accepted answer. A good design treats compute, not storage, as the scarce resource and builds everything around protecting and scaling it.

This is an interview practice design for a LeetCode-like service, built from first principles. It is not a description of how LeetCode is actually built.

Functional Requirements

Users need to browse the problem catalog, narrow it by topic or difficulty, and open a problem to read its statement and examples. They then write a solution in one of several supported languages, such as Java, Python or C++, and submit it.

The judge executes that submission against a hidden test suite and reports one of a small set of verdicts, such as Accepted, Wrong Answer, a timeout (TLE) or a crash during execution. Finally, timed contests let many users compete at once with a live ranking of every participant. Discussion forums, paid tiers and plagiarism detection are valuable features, but they sit beside the judging pipeline rather than inside it, so we leave them out.

Browse problems

See the problem list, filter it, and open a problem.

list · filter

Submit code

Write a solution in Java, Python, C++ and more, then submit.

multi-language

Get a verdict

Code runs against hidden test cases and we report the result.

Accepted · WA · TLE

Contests

Timed contests with a live ranking of every participant.

live leaderboard

  • Out of scope today: discussions, premium plans, plagiarism checks

Non-Functional Requirements

Isolation is the top priority. A submission must never be able to damage the host, reach the network, read secrets or see another user's code. Every other requirement is negotiable before this one is.

After safety comes responsiveness. Someone who hits submit is staring at a spinner, so the verdict should arrive in under about 5 seconds. The system must also tolerate bursts: when a contest opens, traffic can multiply tenfold or more within a minute. And the contest ranking should be near real time, reflecting a newly accepted solution within a few seconds so participants trust what they see.

Secure isolation

User code can never harm our servers or other users.

sandboxed

Fast verdicts

Results come back within a few seconds.

< 5 s

Handle contest spikes

Traffic jumps ten times or more when a contest starts.

burst traffic

Fresh leaderboard

Ranks update within seconds of an accepted solution.

near real-time

Scale Estimation

Assume about 1 million daily active users generating roughly 5 million submissions a day. Spread over 86,400 seconds, that averages to around 60 submissions per second, a load that any reasonably built backend handles comfortably without special tricks.

The average hides the real shape of the load. A weekly contest might draw around 100K participants, and submissions can briefly peak near 1,000 per second. If each run burns roughly 2 seconds of CPU across all its test cases, that peak needs about 2,000 cores busy at once. Code, verdicts and test files are small, so storage barely registers. The estimate points at a single conclusion: the code-running worker fleet is the bottleneck, and the architecture should make that fleet easy to grow and shrink.

1million

Daily active users

≈ 5 million submissions / day

60avg

Submissions per second

5M / 86,400 ≈ 58

100K

Contest participants

peak ≈ 1,000 submits / s

2000cores

Peak worker CPUs

1,000 / s × 2 s each

Compute is the bottleneck, not storage → scale the workers

Core Entities & API

The core entities are Problem (with its hidden test cases), Submission, Contest and Leaderboard. GET /problems/{id} delivers everything the editor needs to render a problem: its description, worked examples, and a starter stub per supported language.

Submitting is asynchronous by design. POST /problems/{id}/submissions takes the language, the source code and an optional contest ID, and immediately returns a submission ID rather than holding the connection open while code runs. The client then calls GET /submissions/{id} until the status moves from pending to a final verdict with runtime and memory figures. During contests, GET /contests/{id}/leaderboard returns the top of the ranking along with the caller's own position, so even someone in 40,000th place can see where they stand without paging through the whole list.

Core entities

ProblemSubmissionContestLeaderboard

API

  • GET /problems/{id}→ { statement, examples, stubs }
  • POST /problems/{id}/submissions { language, code, contest_id? }→ { submission_id }
  • GET /submissions/{id}→ { status, runtime, memory }
  • GET /contests/{id}/leaderboard→ { top_n, my_rank }

High-Level Design

Requests pass through an API gateway responsible for authentication, throttling and routing. Behind it, the submission service validates the code, stores a submission row marked pending, returns the ID to the client, and then enqueues an execution job on Kafka or SQS. The service itself never executes user code.

A pool of code workers pulls jobs from that queue at its own pace. Each worker spins up an isolated container, runs the solution against every test case, and writes the verdict back to the database, where the polling client finds it. For contest submissions, the worker also bumps that contestant's entry in a per-contest Redis sorted set, and leaderboard reads go straight to Redis. Because the front-end services are stateless and the workers are fed by a queue, the worker pool can scale on queue length without anyone else noticing.

status: pendingenqueue jobpull jobsave verdictupdate scoreread ranksClientbrowser · editorAPI gatewayauth · rate limitSubmission svcaccepts codeDatabaseproblems · verdictsJob queueKafka · SQSCode workerssandboxed containersLeaderboardRedis sorted set
High-level design. Dashed arrows are taken only on a cache miss or as a fallback. Scroll sideways on small screens.

Stateless services · workers autoscale on queue length

Deep Dive 1 — Safe Code Execution

Running submissions directly on the application server is the simplest option and the most dangerous. A single malicious script could read environment variables, open outbound connections or crash the process that serves everyone else, and there is no clean way to cap its CPU or memory.

A dedicated virtual machine per run gives superb isolation, but VMs take time to boot and cost real money at a thousand runs per second. A hardened container strikes the balance: it launches in under a second and packs densely onto hosts. Its weakness is the shared kernel, so we reduce the attack surface aggressively. Containers get no network access, a read-only filesystem, an unprivileged user account, and a seccomp filter that rejects risky system calls.

Each run also gets hard ceilings on CPU seconds, memory, number of processes and bytes of output. Runaway loops and fork bombs are terminated before they can starve the host. We choose hardened containers, and for an extra layer we can run them under gVisor or inside Firecracker microVMs and always start from a fresh sandbox, so nothing leaks between submissions.

Run on the server

  • + Simplest to build
  • − One bad script owns the box
  • − No resource limits

Full VM per run

  • + Very strong isolation
  • − Slow to boot
  • − Expensive per run

Hardened containerChosen

  • + Starts in under a second
  • + Cheap, easy to scale
  • − Shared kernel, so harden it
  • No network · read-only filesystem · non-root user · seccomp
  • Hard limits per run: CPU time, memory, process count, output size
  • Extra safety: gVisor or Firecracker microVMs · fresh sandbox every run

Deep Dive 2 — Contest Spikes

The queue is the first line of defense. When a thousand submissions land in one second, they simply wait a moment in line instead of overwhelming the workers. Workers are then autoscaled on queue depth: a growing backlog adds instances, and a draining one removes them, so we pay for peak capacity only during peaks.

Autoscaling reacts with some lag, but contests are on a published schedule, which removes the guesswork. About ten minutes before the start we pre-warm the fleet by launching extra workers and pulling every language image onto those machines, so the first submissions find hot capacity waiting.

Contest jobs also get a dedicated queue and worker pool. That way a surge of casual practice submissions cannot push a contestant's run behind thousands of others. Finally, every participant opens the same handful of problems in the same second, so problem pages are served from a CDN and the origin barely notices the stampede.

  • The queue absorbs the burst

    Submissions wait safely in line instead of overloading the workers.

  • Autoscale on queue depth

    Add workers when the backlog grows; remove them when it drains.

  • Pre-warm before the start

    Contests are scheduled, so scale up and pull images ten minutes early.

  • Separate contest queue

    Contest jobs get their own queue and workers, so practice traffic can't slow them.

  • Cache problem pages on a CDN

    Everyone opens the same four problems at the same second.

Deep Dive 3 — Live Leaderboard

Recomputing a ranking with an SQL sort over 100K rows on every refresh would collapse under contest traffic. Instead, the moment a worker records an accepted contest solution, it computes a single composite score: points multiplied by one million, minus penalty seconds. Higher points always win, and among equal points the participant with less penalty time ranks higher, all expressed as one number.

That score is written with ZADD into a Redis sorted set dedicated to the contest, an update that costs O(log N). The first page of the leaderboard is ZREVRANGE from 0 to 49, which returns the top fifty in descending order. A user's own position comes from ZREVRANK, answered in milliseconds even with 100K entries.

Redis here is a fast, disposable view. Every verdict and score is also persisted in the database, which remains the source of truth, so if the Redis node is lost the sorted set can be rebuilt from stored results.

  1. 1AcceptedWorker sees a new accepted solution in a contest
  2. 2Encode the scorepoints × 10⁶ − penalty seconds → one number
  3. 3ZADD to RedisSorted set per contest; O(log N) update
  4. 4Read the top NZREVRANGE 0 49 → first page of ranks
  5. 5Find my rankZREVRANK user → rank in milliseconds
  • Redis is the fast view · the database stays the source of truth

Trade-offs

Clients poll for verdicts instead of receiving pushes. Polling once a second for a few seconds is simple and cheap; WebSockets would feel slightly snappier but require keeping many long-lived connections open. Hardened containers are faster and cheaper than microVMs, which in turn offer stronger isolation, so the choice depends on how much risk the platform tolerates.

The asynchronous queue absorbs spikes and decouples submission handling from execution, at the cost of a small added delay on every run. Keeping the leaderboard in memory makes ranking instant, but only because the database backs it for recovery. The shape of the whole design fits in one line: sandbox every submission, buffer execution behind a queue, and keep rankings in RAM.

  • Polling vs push for verdicts

    Polling is simple and fine for a few seconds; WebSockets need long-lived connections.

  • Containers vs microVMs

    Containers are faster and cheaper; microVMs give stronger isolation.

  • Asynchronous queue

    Absorbs spikes and decouples services, but adds a little delay.

  • Leaderboard in memory

    Instant ranks, but Redis must be backed by the database.

Isolate untrusted code. Queue the work. Rank in memory.

Key takeaways

  • Compute, not storage, is the bottleneck: assume around 2,000 cores at a peak of 1,000 runs per second.
  • Never execute user code on API servers; use hardened, resource-limited sandboxes on a separate worker fleet.
  • Queue submissions, autoscale workers on queue depth, and pre-warm capacity before scheduled contests.
  • Give contests their own queue and worker pool so practice traffic cannot delay them.
  • Rank contestants with a Redis sorted set using one composite score, backed by the database.

Interview questions

How does an online judge run untrusted code safely?

Each submission runs in a fresh, hardened container on a separate worker fleet, never on the API servers. The sandbox has no network, a read-only filesystem, a non-root user and a seccomp filter on system calls. Hard limits on CPU time, memory, processes and output stop runaway code. For stronger isolation, containers can run under gVisor or inside Firecracker microVMs.

How do you stop an infinite loop in a submitted solution?

Enforce a hard CPU time limit on every run, sized to the problem's expected complexity. The worker monitors the sandbox and kills it the moment the limit is exceeded, then records a Time Limit Exceeded verdict. Process-count limits catch fork bombs the same way, so one bad submission cannot hold a worker or affect anyone else.

How does a coding platform handle traffic spikes during a contest?

Submissions go onto a queue so bursts wait briefly instead of overloading workers. Workers autoscale on queue depth, and because contests are scheduled, the fleet is pre-warmed with language images about ten minutes early. Contest jobs get their own queue and workers so practice traffic cannot slow them, and problem pages are cached on a CDN.

How do you build a real-time leaderboard for 100,000 contestants?

Store one Redis sorted set per contest. Encode each user's result as a single score, such as points times one million minus penalty seconds, and update it with ZADD in logarithmic time. ZREVRANGE returns the top page and ZREVRANK gives any user's position in milliseconds. Keep scores in the database too so the set can be rebuilt.

Why is code submission asynchronous instead of a single request?

Running code takes seconds and its demand is bursty, so holding an HTTP connection open would tie up servers and fail under contest load. Returning a submission ID immediately and queuing the job lets workers process at their own pace. The client polls for the verdict, which is simple and adds little overhead for a wait of a few seconds.

Keep practising