The Blueprint
Infrastructure intermediate

Design an online judge (LeetCode)

Run untrusted user code against test cases with time + memory limits — sandboxing is the design problem, everything else is CRUD.

asked at LeetCode · HackerRank · Codeforces Rarely asked ~35 min walkthrough ✎ Practice on canvas →
01

Scope the requirements

Two seams: sandboxed execution (the interesting security + isolation problem) and submission workflow (queue + result). The interviewer scores on knowing why running arbitrary code needs more than 'run it in a container'.

Functional — what it must do

  • Users submit code (Python, C++, Java, Go, JS) against a problem.
  • Server runs code against N test cases with time + memory limits.
  • Return verdict: Accepted / Wrong Answer / TLE / MLE / RE per test case.
  • Persist submission history + leaderboard by problem.
  • Explicitly cut: contests, discussion, editorial — offer them back.

Non-functional — the numbers that shape the design

  • Verdict latency: under 10 s for typical problems (10 test cases).
  • 50k submissions/hour peak (contest windows push this 10×).
  • Isolation: user code cannot access the internet, other submissions, or the host.
  • Fairness: contest submissions get FIFO queue; non-contest can be de-prioritised.
  • 99.9% on submission accept; execution may retry.
Say this out loud

The whole system is a queue on the front, a sandbox pool in the middle, and a durable submission log at the back. Sandbox design is where interviews live or die.

02

Back-of-envelope estimates

Submission volume is small; per-submission compute is the scale story. During contests, everything runs 10-100× hotter than steady state.

QuantityEstimateHow you got there
Submissions/hour~50k peak, 500k contest peakone submission runs ~5 s across ~10 test cases.
Sandbox runtime~5 s per submission10 test cases × ~500 ms each. Time limit typically 1-3 s per case.
Concurrent sandbox workers~1000 during contests50k/h × 5 s / 3600 s × safety = 100-1000.
Storage submissions~5 TB/year50k/h × 24 × 365 × ~10 KB (code + verdict). Postgres.
Test cases per problem~10-100hidden + sample. Stored as tar.gz in S3.
Language runtimes~10each pre-baked into a base image with compilers + libs pinned.
Say this out loud

1000 sandbox workers × 5 s = 5000 CPU-seconds/s at contest peak — that's a lot of hardware but the shape is bounded and predictable.

03

Sketch the API

Submission is a POST that returns an id; verdict polls or subscribes. Streaming stdout during execution is a nice-to-have.

POST/api/v1/submissionsbody: { problemId, language, code }. → { submissionId, status: queued }.
GET/api/v1/submissions/{id}→ { status, verdict, testResults[], runtime, memory }.
WS/rt/submissions/{id}Push status updates: queued → running → passed_case_3 → verdict.
GET/api/v1/problems/{id}/leaderboard→ top submissions by runtime + memory.
GET/api/v1/problems/{id}→ problem statement + sample cases (public). Test cases stay server-side.
  • Submission returns immediately with an id; execution is async. Client subscribes to the WS for live updates or polls.
  • Test cases are never sent to the client — user code runs on our sandbox, we return only verdict + failing input for the first-failing case.
04

Data model

Small structured data (submissions, problems, users) + blob storage for code + test cases. The queue itself is the operational spine.

submissions
id PK · user_id · problem_id · language · code · verdict · runtime_ms · memory_kb · submitted_at
Postgres, sharded by user_id. Hot on leaderboard queries.
problems
id PK · statement · time_limit · memory_limit · test_case_ref
Postgres. Small; heavily cached.
test_cases
problem_id → tar.gz in S3
One archive of stdin/expected pairs per problem. Downloaded by sandbox workers on first run + cached.
queue
submission_id · priority · problem_id
Kafka or SQS. Priority queue: contest > paid > free.
leaderboard
(problem_id, runtime) → user_id
Redis ZSET per problem. Fastest solutions ranked.
The database call

Postgres for submissions because they're relational + queried by user_id + problem_id. Kafka for the queue because it gives replay + priority partitions. S3 for test cases because they're immutable + large + read many times.

05

High-level design

Three tiers: web (accepts submissions), queue (distributes to workers), sandbox pool (runs code). The isolation layer is the load-bearing wall.

client edge service sandbox pool data User API gateway Submission svc Result svc Realtime push Priority queue kafka Sandbox worker gvisor / firecracker × N Submissions PG S3 test cases Leaderboard enqueue test cases insert queued progress verdict
sandbox workers pull from queue, run isolated, write verdicts. results flow back through pg + realtime push.
  1. User POST /submissions → submission service inserts row with status=queued → publishes id to priority queue → returns id.
  2. Client subscribes to /rt/submissions/{id} for live updates.
  3. Sandbox worker pulls next submission from queue → downloads test cases from S3 (cached locally per problem) → starts a fresh sandboxed container per submission.
  4. Runs user code against each test case with time + memory limits; streams progress events to realtime push service.
  5. On completion: writes final verdict + timing to Postgres, updates the problem's leaderboard ZSET in Redis, tears down the sandbox.
06

Deep dives — where the interview is won

Phase 01

Sandbox: containers alone are not enough

A Docker container is not a security boundary against hostile code — a kernel bug lets user code escape. For running untrusted code at scale, the standard is one of two layers: gVisor (a user-space kernel intercepting syscalls) or Firecracker microVMs (KVM-based, tiny, seconds to boot). Both isolate the guest kernel from the host.

Inside the sandbox, enforce cgroups CPU + memory limits, disable network namespaces (no internet), mount a read-only filesystem with just the test case as writable temp. Kill the sandbox at time limit + a grace period. Cleanup is nuclear — destroy the sandbox, don't reuse. Reuse leaks state between users.

user code isolation host Submitted code gVisor / Firecracker syscall intercept cgroups + no-net Host kernel restricted syscalls
user code sees a filtered syscall interface. no direct host kernel access.
Trade-offFirecracker boots ~125 ms vs Docker's ~1 s — matters for sub-second execution. gVisor is slightly slower for I/O-heavy code but wraps existing Docker workflows. Pick per workload.
Phase 02

Priority queue: contest submissions cannot wait

During a contest, submissions must be judged in FIFO order — a delayed verdict changes rankings. Non-contest submissions can wait 30+ seconds without complaint. Kafka partitioning by priority: queue.high for contest, queue.normal for daily practice, queue.low for exploration. Worker pool consumes with weighted allocation — say 80% workers on high, 15% normal, 5% low.

During a rush, high-priority queue absorbs all workers until it drains. This risks starving normal-priority; add a shedding mechanism: if normal queue lag exceeds ~5 min, redirect some workers there. The knob is the fairness policy: it's a business decision, not a technical one.

submissions queues workers Contest Daily high normal Worker pool 80/15/5 split priority
priority partitions + weighted worker pool. shed on lag to avoid starvation.
Trade-offPriority queues are notoriously hard to get right — over-share to low priority hurts contests; under-share starves practice. Monitor queue lag as a first-class metric and adjust weights dynamically.
Phase 03

Test case delivery: never in the response, always at the sandbox

Test cases stay server-side forever. Sending them to the client would let users train against private tests. Sandbox workers download the test case archive (tar.gz from S3) on first run for a problem, then cache locally — most problems get many submissions, so the archive lives in the worker's disk warm cache.

Verdict returned to the user is coarse-grained: 'passed', 'failed on test 5', with the failing input possibly shown only for the first sample case (public). Hidden test failures show the case number but not the input. This is a leak-prevention design; a subtle test case is the whole IP.

worker local cache s3 Sandbox worker Local test archive warm on hot problems S3 origin hit miss (first run)
test cases live on the worker. never exposed to user code except as stdin.
Trade-offIf a hot problem's test archive changes (bug in the case), you must invalidate the local cache on all workers. Version the archive per problem; workers check version on each run and re-download if stale.
07

Follow-ups you should expect

How do you support 10 languages?
Each language has a base container image with compiler + runtime + common libraries. Sandbox runs a wrapper script: compile (if needed) with a compile time limit, then run against each test case. Language-specific quirks (JVM warm-up, Python startup cost) get accounted for in the time limit calibration.
Judging fairness across hardware — same code, different runtime?
Yes, this is a real problem. Two options: (1) run all workers on identical hardware + isolate CPU with taskset to avoid noisy neighbours, (2) run each submission twice on different boxes and take the median. Most competitive programming platforms do (1) and warn users about small runtime variance.
Can users see test cases from stack traces or side channels?
Language stack traces can reveal file paths, memory addresses. Sandbox scrubs paths from stderr; disables coredumps; runs in a fixed cwd. Users can attempt timing attacks (measure runtime to infer test cases) — for competitive programming this is acceptable, for production interview platforms tests are randomised or shielded further.
What happens to a submission mid-crash of a worker?
Kafka consumer group offset only advances after verdict is written. If worker dies, another worker pulls the same submission on next rebalance. The submission was atomic — either it committed verdict or it will be retried. User sees a small delay, not a lost submission.
Leaderboard integrity — someone submits, then edits code to game?
Every submission is immutable in Postgres. Leaderboard shows the earliest-timestamped Accepted submission per user per problem, with the recorded runtime. Editing code creates a new submission with a new id and timestamp — doesn't overwrite the leaderboard entry.

Where candidates lose the room

  • Running user code in plain Docker — kernel escape is real; use gVisor or Firecracker.
  • Reusing sandboxes across submissions — a subtle side effect leaks between users.
  • Sending test cases to the client — you lose the IP the moment one leaks.
  • Postgres as the submission queue — polling scales worse than Kafka's push semantics.
  • One queue for all priorities — a contest spike blocks everyday practice submissions for hours.

Concepts this leans on

Found a bug or want more?

Tell me what's broken, missing, or wrong. Every message lands in my inbox.