System Design (High-Level Design) Module 29 – Case studies: platforms, analytics and developer tools
Design an online judge like LeetCode
Run untrusted code against hidden tests with strict limits, absorb contest bursts with queues and fair scheduling, and return fair, repeatable verdicts.
What you will learn
- Estimate the cores a contest burst needs from its arrival rate and the work per submission
- Run untrusted code safely with strict limits on CPU, memory, processes and output
- Scale judging for contest bursts with queues that are fair between users
- Protect hidden tests and make verdicts fair and repeatable
Before you start
What you’ll learn · Before you start
What you will learn
- Estimate the cores a contest burst needs from its arrival rate and the work per submission
- Run untrusted code safely with strict limits on CPU, memory, processes and output
- Scale judging for contest bursts with queues that are fair between users
- Protect hidden tests and make verdicts fair and repeatable
Before you start
On this page
Not affiliated
Based on public information about LeetCode. MySmartCoPilot is not affiliated with LeetCode, and LeetCode’s real architecture may differ from what this lesson describes.
A contest of 40,000 people opens at a fixed time, and for its first minutes an online judge must compile and run 167 submissions a second, code from strangers, against tests its authors have never seen. Each one gets a verdict: whether it passed, with the time and memory it used.
The core idea is to treat every submission as hostile and every contest start as a flood. Each run happens in a fresh sandbox with hard limits on CPU time, wall-clock time, memory, processes and output, and with no network. Submissions go through a queue to a pool of judge workers that is sized, and started, before the contest begins, and the queue shares those workers fairly between users. Hidden tests stay on the judging side; only verdicts and per-test numbers come back. Security, bursts and fairness shape everything else.
Ten languages, weekly contests and a verdict within 10 seconds
Ask these questions first; the second column holds the answers assumed here:
| Question | Assumed here |
|---|---|
| Which languages must run? Compiled ones add a compile step with its own limits | About ten |
| Do contests happen, and how many people join one? | Weekly, 40,000 people |
| How quickly must a verdict arrive? | Within 10 seconds outside contests |
| Is a submission judged on all tests, or does it stop at the first failure? | Every test, with a result per test |
Functional:
- Submit code for a problem in a chosen language.
- Compile it, run it against the problem’s sample and hidden tests, and return a verdict: Accepted, Wrong Answer, Time Limit Exceeded, Run-Time Error, or Compile Error, with time and memory per test.
- Show progress while judging runs, and the verdict when it ends.
- Run contests with a scoreboard, and judge a whole problem again when its tests change.
Non-functional:
- Isolated: no submission can read the tests, reach the network, see another submission or harm the host.
- Fair: the same code gets the same verdict, measured on the same hardware with the same limits.
- Fast: 95 % of verdicts within 10 seconds outside contests and within 60 seconds in a contest’s first minutes.
- Durable: an accepted submission is never lost, and every verdict can be explained later.
Checkpoint
Check: What the judge must guarantee
The estimate: 167 submissions a second keep 1,190 cores busy
Assume 1.2 million practice submissions a day, a contest whose 40,000 people send 100,000 submissions in the first 30 minutes with the busiest minutes at three times that pace, and about 5 CPU-seconds per submission to compile and run some 40 tests. The script turns that into the numbers that shape the design:
"""The online judge's estimates from stated assumptions: change any of them and run it again."""
DAY = 86_400 # seconds
practice_per_day = 1_200_000 # submissions outside contests
practice_peak = 3 # the busiest hour against the day's average
contest_people = 40_000
contest_submissions = 100_000 # in the first 30 minutes
contest_window = 30 * 60 # seconds
contest_peak = 3 # the busiest minutes against the contest's average
cpu_seconds = 5 # per submission: compiling and about 40 tests
busy_share = 0.7 # keep cores at most 70 % busy, so that timings stay steady
cores_per_machine = 8
code_bytes = 2_000 # per submission
result_bytes = 2_000 # per submission: time and memory for each test
problems = 3_000
test_data_mb = 20 # per problem
practice_avg = practice_per_day / DAY
contest_avg = contest_submissions / contest_window
contest_top = contest_avg * contest_peak
busy = contest_top * cpu_seconds
cores = busy / busy_share
stored = practice_per_day * (code_bytes + result_bytes) * 365
print(f"Practice: {practice_avg:.1f} submissions a second on average, {practice_avg * practice_peak:.1f} at its peak")
print(f"Contest: {contest_avg:.1f} a second over the first 30 minutes, {contest_top:.0f} in the busiest minutes")
print(f"Busy cores in those minutes: {contest_top:.0f} x {cpu_seconds} CPU-seconds = {busy:,.0f}")
print(f"Cores to have running at {busy_share:.0%} busy: {cores:,.0f} ({cores / cores_per_machine:,.0f} machines of {cores_per_machine})")
print(f"Code and results kept for a year of practice: {stored / 1e12:.2f} TB")
print(f"Test data for {problems:,} problems: {problems * test_data_mb / 1e3:.0f} GB") Output
Practice: 13.9 submissions a second on average, 41.7 at its peak Contest: 55.6 a second over the first 30 minutes, 167 in the busiest minutes Busy cores in those minutes: 167 x 5 CPU-seconds = 833 Cores to have running at 70% busy: 1,190 (149 machines of 8) Code and results kept for a year of practice: 1.75 TB Test data for 3,000 problems: 60 GB
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 estimates.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Compute, not storage, shapes this system. 167 submissions a second at 5 CPU-seconds each keep 833 cores busy, the arrival rate times the time each one takes (Little's law: In a stable system, the average number of requests inside equals the arrival rate times the average time each one spends inside, L = λW. At 100 requests a second and 50 ms each, about 5 are in progress. More in “Little's law, utilisation and server counts”), and running cores at most 70 % busy, so that one run does not slow another, means about 1,190 cores, or 149 machines of 8 cores, during the busiest minutes. Practice traffic at its own peak needs about a quarter of that. Storage is minor: about 1.75 TB a year of code and results, and 60 GB of test data in total.
Checkpoint
Check the estimate
Submissions are accepted at once and judged later
POST /submissionswith the problem, the language and the code: it answers202 Acceptedwith a submission id, because judging takes seconds.GET /submissions/{id}: the status, and once judged, the verdict and the time and memory of each test.GET /submissions/{id}/events: the same progress as a stream, test by test.GET /contests/{id}/scoreboard: the standings, cached for a few seconds.
| Record | Holds |
|---|---|
| Submission | Its id, user, problem, language, status, verdict, worst time and peak memory, and a pointer to the code in object storage |
| Result, one per test | The test number, verdict, time and memory |
| Problem | Pointers to versioned test sets, each identified by the hash of its files, so a verdict always records which tests it was judged against |
Checkpoint
Check: Accepted now, judged later
The high-level design, walked along one submission
The online judge's high-level design
Text description of the diagram
The diagram runs from top to bottom.
- In the browser, a user writes code and submits it with a POST to the submission API.
- The submission API checks the user, the size of the code and the user's rate, stores the submission in the submissions database (the code itself in object storage), and puts the submission id on the judge queue.
- The judge queue is fair between users and serves contest work first.
- Judge workers pull one submission at a time. They hold no credentials and have no network access.
- For each run, a worker starts a sandbox, a microVM or a user-space kernel with cgroup and seccomp limits, and in it compiles the code and runs each test.
- The test data is mounted into the sandbox read-only, as copies identified by version. Hidden tests are never sent out of the judging side.
- The worker sends the result of each test to the results service, which records the verdict, the time and the memory of every test, updates the scoreboard and stores everything in the database.
- A dashed arrow goes from the results service back to the browser: the user sees progress and then the verdict.
The write path. The API checks the user, the code’s size and how often this user has submitted, stores the code and the row, and puts the submission id on the judge queue. Nothing judges inside the API, so a burst only makes the queue longer.
The judging path. A worker takes one submission, starts a fresh sandbox, copies the code in and mounts the test set read-only. It compiles the code under its own limits, then runs each test with limits on CPU, wall-clock time, memory, processes and output, and compares the output with the expected answer. Results go to the results service test by test, so the user sees progress.
The read paths. Status comes from the database, progress from the event stream, and problem pages and the scoreboard from caches, so none of them competes with judging. Step through one made-up submission, from the browser to its verdict:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- Submit. A user submits a C++ solution to a problem. The API checks who they are, the size of the code and how often they have submitted in the last minute.
- Accepted at once. It stores the code and the row, puts the submission id on the judge queue and answers 202 Accepted with the id, s-77. Nothing is judged inside the API, so a burst only makes the queue longer.
- Taken from the queue. A judge worker takes the next submission from the queue, which shares the workers fairly between users.
- A fresh sandbox. The worker starts a fresh sandbox, copies the code in and mounts the problem's test set read-only, by the hash of its files. It compiles the code under limits of its own.
- Test by test. Each test runs with limits on CPU time, wall-clock time, memory, processes and output, and its result goes back as soon as it is known.
- Progress on screen. The browser follows the results on the event stream, test by test.
- The verdict. After the last test, the worker reports the verdict with the worst time and the peak memory. The hidden tests never left the judging side; only verdicts and numbers did.
- Explained later. The verdict is stored with the hash of the test set it was judged against, so it can be explained, or judged again, if the tests ever change.
Checkpoint
Check: The write, judging and read paths
Practise: the first design, one question at a time
Practise the four ideas above, the weakest first:
Practice that adapts to you
Practise this lesson
10 questions about 11 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
The deep dives this design must answer
Three groups of choices remain, and each has a short list of options:
- Isolation: containers with seccomp and cgroups share the host’s kernel and start fastest. gVisor puts a kernel written in Go between the program and the host. Firecracker microVMs give each submission its own virtual machine.
- Queueing: first come, first served is the simplest order. Round robin between users takes one submission from each waiting user in turn. A per-user cap refuses new submissions while a user already has several waiting.
- Progress: polling asks for the status every second or two. Server-sent events keep one HTTP response open and push each result (HTML standard). WebSockets carry messages both ways.
MySmartCoPilot’s own exercises are a useful contrast. They run learners’ code in the learners’ browsers, on a separate sandbox origin, so each run uses the visitor’s own machine. The price is that the tests travel to the browser, where anyone can read them, so those checks are feedback, not proof: only a server-side judge can keep tests hidden.
The deep dives answer six questions: how to run hostile code without letting it out, which limits each layer must enforce, how to absorb the first minutes of a contest, how to keep one user from delaying everyone, how to make verdicts fair and repeatable, and how to keep hidden tests hidden.
Sandboxing untrusted code, layer by layer, with the limits reproduced Pro: opens with a Pro, Premium or Ultimate pass
No single mechanism makes hostile code safe, so a judge stacks layers that each stop something different. This part compares the isolation choices with their published costs and reproduces the limits a run must enforce.
In this part:
- What each isolation layer stops, and its trade-offs in numbers
- Limits that hold: CPU, wall clock, memory, processes and output
1 runnable example (Python) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Read a complete sample first: Design a real-time leaderboard
Contest bursts: fair queues, warm workers and capacity, with numbers Pro: opens with a Pro, Premium or Ultimate pass
A contest is the one event whose start time is known in advance, so the judge can be ready before the flood instead of reacting to it. This part measures three queue policies on the same burst and works out how many workers must be warm when the contest opens.
In this part:
- Why first come, first served fails at a contest start, measured
- Workers warm before the start, worked through
- Contest first, practice later, and limits on resubmission
- Bottlenecks at ten and a hundred times the load
1 runnable example (Python) · about 6 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Fair verdicts, hidden tests and failure drills Pro: opens with a Pro, Premium or Ultimate pass
A verdict is only worth something if the same code gets the same answer every time and the tests stay unseen. This part reproduces the comparison and timing problems that make verdicts unfair, then drills the failures a judge must survive.
In this part:
- Comparing output: exact bytes, tokens and tolerances
- Time limits from reference solutions, and flaky verdicts reproduced
- Keeping hidden tests hidden
- Failure drills: a dead worker, changed tests and a backlog
- What each level is expected to cover
2 runnable examples (Python) · about 10 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
Why does an online judge limit both CPU time and wall-clock time for every run?
CPU time measures the work a program does, so it is the fair limit: it barely changes when the machine is busy with other runs. But a program that sleeps, waits for input that never comes or blocks on a lock uses almost no CPU, and under a CPU limit alone it would hold its worker forever. A wall-clock limit, usually a few times the CPU limit, catches those runs. The kernel enforces the CPU limit, through RLIMIT_CPU or a cgroup's CPU accounting, and the judge enforces the wall-clock deadline by killing the run. Either one ends in Time Limit Exceeded.
-
Mid to senior
A contest of 40,000 people opens at a fixed time, and its first minutes bring 167 submissions a second at about 5 CPU-seconds each. How many workers do you need, when must they be running, and how do you stop a few heavy users from delaying everyone else?
-
Senior to staff
You must run untrusted C++, Java and Python submissions. Choose between containers with seccomp and cgroups, gVisor and microVMs, and defend the choice with what each layer stops and what it costs per submission.
-
Mid to senior
A participant reports that their solution got Time Limit Exceeded on test 17, while an identical resubmission a minute later was accepted. How do you investigate, and how do you make verdicts repeatable?
Key takeaways
- An online judge is a queue in front of sandboxes: the API stores and queues, workers judge, and results come back test by test.
- Contest load is compute: 167 submissions a second at 5 CPU-seconds each keep 833 cores busy, about 1,190 at 70 %.
- Every run needs limits on CPU time, wall-clock time, memory, processes and output, and no network.
- Tests that run in a learner’s browser are feedback, never proof; hidden tests must stay on the judging side.
References
- gVisor documentation (The gVisor Authors)
- Firecracker (The Firecracker project)
- Control Group v2 (Linux kernel documentation) (The Linux kernel documentation)
- Seccomp BPF, SECure COMPuting with filters (Linux kernel documentation) (The Linux kernel documentation)
- resource, resource usage information (Python documentation) (Python Software Foundation)
- Problem Package Format, version 2025-09 (ICPC)
- Analysis and simulation of a fair queueing algorithm (Demers, Keshav and Shenker) (ACM SIGCOMM)
- HTML Living Standard: server-sent events (WHATWG)
- RFC 6585: Additional HTTP Status Codes (429 Too Many Requests) (IETF)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress