LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 178 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/8
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/11
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageClustering and PCACore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/24
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernanceData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/19
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnAI Lab InterviewingAI Lab Coding Interview: Python Systems
⚙️HardMLOps & Deployment

AI Lab Coding Interview: Python Systems

Build production-shaped Python systems under staged requirements: crawlers, TTL stores, schedulers, token buckets, ledgers, and thread-safe claim points.

28 min read
Learning path
Step 158 of 178 in the full curriculum
Reasoning Agent System DesignAI Lab System Design Interview

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

You just designed a reasoning agent that budgets test-time compute and recovers from failed tool calls. A lab coding round asks a smaller question: can you implement one slice of that system while the requirements keep moving?

You're in a shared editor with a starting URL and a get_links callback. Twelve minutes later they want worker fanout. Then a per-host rate limit. Then cancellation. People who stall didn't forget BFS. They lost the visited invariant the moment the second requirement arrived.

OpenAI's engineering interviews look for well-designed solutions, high-quality code, performance, and test coverage, plus communication you can follow while you work.[1]Reference 1Interview guidehttps://openai.com/interview-guide/ Anthropic's technical process uses live coding tools such as Colab and CodeSignal; they expect you to be fluent in syntax and the standard library so lookup time doesn't eat the round, and they'll ask about experience and motivation.[2]Reference 2Careershttps://www.anthropic.com/careers Formats vary by team. Practice that bar with one base prompt and staged follow-ups: TTLs, concurrency, cancellation, rate limits, deterministic output, and a state model that still makes sense when the next constraint lands.

Keep one invariant visible

Use this loop for every prompt:

  1. Restate input, output, and failure behavior.
  2. Ship version 1 with the smallest correct state model.
  3. Add table-driven tests before adding stage 2.
  4. Isolate shared mutable state before adding threads.
  5. End by naming complexity, race risks, and production hardening.

The code that survives a staged round is the version whose visited set, TTL record, or ledger key you can still defend after the next requirement lands.

TimeTarget
0-5 minclarify ordering, failure behavior, and mutable state
5-20 minlevel 1 complete with tests
20-32 minlevel 2 or 3 complete without rewriting
32-38 minedge tests and complexity
final minutesname the next follow-up, the race risk, and what you'd harden in production

What is the common failure mode in staged coding rounds?

Answer

Losing the state model. People often solve version 1, then bolt on TTLs, transactions, or concurrency until nobody owns visited, the TTL record, or the ledger key. Keep those structures explicit and add one invariant at a time.

Python tools to know cold

Know these without documentation:

NeedPython building block
FIFO work queuecollections.deque, queue.Queue, asyncio.Queue
Counts and top errorscollections.Counter
LRU cachecollections.OrderedDict
Deadlines and TTLtime.monotonic, injected now function
Priority schedulingheapq, queue.PriorityQueue
Thread safetythreading.Lock, threading.RLock, threading.Condition, threading.Event
Worker fanoutconcurrent.futures.ThreadPoolExecutor, as_completed
Parsingsplitlines, re, explicit state machines

Don't wait for a test framework. Write a small run_tests() and use plain assert.

Why inject now instead of calling time.sleep() in a TTL or rate-limit interview test?

Answer

Injected time makes edge cases deterministic. You can test expiry, refill, and retry-after math immediately instead of slowing the interview down or creating flaky tests.

Same-host crawler

The running example is a CI-docs crawler. You're given https://repo.example/start and get_links(url) -> list[str]. Visit only URLs on the same host. Return them in deterministic breadth-first order for the single-threaded version. Don't visit the same URL twice. Ignore malformed URLs and off-host URLs.

Clarifying questions to ask before you type:

  • Should URLs be normalized for fragments, trailing slashes, query strings, and redirects?
  • Should failed fetches be retried, skipped, or returned separately?
  • Does deterministic order still matter after worker fanout, or only for the single-threaded version?

The invariant is: a URL enters the queue only after it has been accepted into visited. Put host checks and that claim before fetch. Relative links such as /tests need urljoin so they become https://repo.example/tests.

Diagram showing candidate URL, claim into visited, fetch outside lock, and already claimed.
candidate URL, claim into visited, fetch outside lock, and already claimed.
same-host-crawler.py
1from collections import deque 2from urllib.parse import urlparse, urljoin 3 4PAGES = { 5 "https://repo.example/start": ["/tests", "/ci", "https://other.example/x"], 6 "https://repo.example/tests": ["/ci"], 7 "https://repo.example/ci": [], 8} 9 10def crawl(start_url: str, get_links) -> list[str]: 11 start = urlparse(start_url) 12 if not start.scheme or not start.netloc: 13 return [] 14 15 visited = {start_url} 16 queue = deque([start_url]) 17 ordered: list[str] = [] 18 19 while queue: 20 url = queue.popleft() 21 ordered.append(url) 22 23 for raw_link in get_links(url): 24 candidate = urljoin(url, raw_link) 25 parsed = urlparse(candidate) 26 if parsed.scheme not in {"http", "https"}: 27 continue 28 if parsed.netloc != start.netloc: 29 continue 30 if candidate in visited: 31 continue 32 visited.add(candidate) 33 queue.append(candidate) 34 35 return ordered 36 37visited_order = crawl("https://repo.example/start", lambda url: PAGES.get(url, [])) 38print(visited_order) 39assert visited_order == [ 40 "https://repo.example/start", 41 "https://repo.example/tests", 42 "https://repo.example/ci", 43]
Output
1['https://repo.example/start', 'https://repo.example/tests', 'https://repo.example/ci']

/tests is enqueued before /ci, so BFS returns start, tests, then ci. The off-host https://other.example/x never enters visited. When tests later points at /ci, the claim already happened, so you don't fetch it twice.

That single-threaded version still has a hole once two workers can call get_links at the same time. If you fetch first and mark visited later, both workers can pick /ci.

Two workers race for https://repo.example/ci. The left path fetches before marking visited, so both workers call get_links. The right path adds /ci to visited under a lock, then fetches outside the lock, so worker B skips.
Two workers race for `/ci`. Fetch-then-mark lets both call `get_links`. Claim-then-fetch adds `/ci` to `visited` under the lock first, so only one worker fetches.

Protect the claim step, not the whole fetch. Deterministic return order gets expensive once workers run in parallel, unless the prompt still requires it. Say that out loud before you add a global lock just to keep BFS order.

crawler-claim-step.py
1from threading import Lock 2 3visited = {"https://repo.example/start"} 4visited_lock = Lock() 5 6def claim(candidate: str) -> bool: 7 with visited_lock: 8 if candidate in visited: 9 return False 10 visited.add(candidate) 11 return True 12 13print(claim("https://repo.example/ci")) 14print(claim("https://repo.example/ci")) 15print(sorted(visited))
Output
1True 2False 3['https://repo.example/ci', 'https://repo.example/start']

For rate limiting, add a per-host token bucket or next-allowed timestamp before fetch. For cancellation, check a threading.Event before scheduling new work, before fetching, and after each fetch before enqueueing children. Name all three behaviors: duplicate prevention, bounded fetch concurrency, and a cooperative stop.

Claim-before-fetch still leaves failed fetches stuck unless you model more than one terminal state:

StateMeaningRetry?
in_flightClaimed, fetch not finishedNo second worker
visited / successContent processedNo
failed / retry setHard failure with policy remainingYes, under backoff

When the interviewer asks about failed fetches, say: the claim still happens under the lock, but success and permanent failure are different outcomes. A production sketch keeps visited for terminal success, in_flight for active work, and a retry queue with attempt counts so a timeout doesn't become permanent silence.

What invariant should you say before adding threads to a crawler, scheduler, or cache?

Answer

Name the shared state and the exact claim point. For a crawler, a URL is inserted into visited while holding the lock before any worker can fetch or enqueue it.

The crawler taught claim-before-mutate. The next prompt asks the same ownership question on a store: every public read has to observe expiry, not just get.

TTL key/value store

Implement an in-memory store with set(key, value, ttl=None), get(key), delete(key), and scan(prefix). ttl is in seconds. Expired keys behave as missing. Tests must not call sleep.

Ask:

  • Should scan(prefix) return keys, values, or key/value pairs?
  • Is a key expired when expires_at == now, or only when expires_at < now?
  • Should compare-and-set treat an expired key as missing?

Inject now so tests can jump the clock. Store expires_at beside each value. The invariant is: every public read path either returns a non-expired value or removes the expired key. This store treats equality as expired (expires_at <= now), so a key set at time 10.0 with ttl=5.0 is gone at 15.0.

ttl-store.py
1from dataclasses import dataclass 2from typing import Callable 3 4@dataclass 5class Entry: 6 value: str 7 expires_at: float | None 8 9class Store: 10 def __init__(self, now: Callable[[], float]) -> None: 11 self.now = now 12 self.items: dict[str, Entry] = {} 13 14 def set(self, key: str, value: str, ttl: float | None = None) -> None: 15 expires_at = None if ttl is None else self.now() + ttl 16 self.items[key] = Entry(value, expires_at) 17 18 def get(self, key: str) -> str | None: 19 entry = self.items.get(key) 20 if entry is None: 21 return None 22 if entry.expires_at is not None and entry.expires_at <= self.now(): 23 self.items.pop(key, None) 24 return None 25 return entry.value 26 27 def delete(self, key: str) -> None: 28 self.items.pop(key, None) 29 30 def scan(self, prefix: str) -> list[str]: 31 return sorted( 32 key 33 for key in list(self.items) 34 if key.startswith(prefix) and self.get(key) is not None 35 ) 36 37clock = {"now": 10.0} 38store = Store(lambda: clock["now"]) 39store.set("task:1", "running", ttl=5.0) 40store.set("task:2", "ready") 41print(store.get("task:1"), store.scan("task:")) 42clock["now"] = 15.0 43print(store.get("task:1"), store.scan("task:"))
Output
1running ['task:1', 'task:2'] 2None ['task:2']

scan calls get, so expiry cleanup isn't a get-only side path. Snapshot list(self.items) first; get may pop keys while you iterate.

Compare-and-set and locking

Compare-and-set must clean any expired value and compare while holding the same lock. The compact example below omits TTL so the atomic boundary is easy to see:

compare-and-set.py
1from threading import RLock 2 3class AtomicStore: 4 def __init__(self) -> None: 5 self.items: dict[str, str] = {} 6 self.lock = RLock() 7 8 def compare_and_set(self, key: str, expected: str | None, value: str) -> bool: 9 with self.lock: 10 if self.items.get(key) != expected: 11 return False 12 self.items[key] = value 13 return True 14 15store = AtomicStore() 16print(store.compare_and_set("task:1", None, "running")) 17print(store.compare_and_set("task:1", None, "ready")) 18print(store.compare_and_set("task:1", "running", "ready"))
Output
1True 2False 3True

For the TTL store, wrap every public method with the same lock. If compare_and_set() calls get() and set() while holding that lock, use an RLock or split the internal helpers so the lock is acquired once. What you have to defend is atomicity: no other thread can change the key between compare and set.

Expiry is a time boundary on one record. A scheduler adds a graph: some tasks are ready, some retry, and some never run.

Task scheduler with retries

Schedule CI tasks with dependencies. A task becomes runnable when all dependencies have completed. Detect dependency cycles before running anything. Run ready tasks in priority order (lower number first). Retry failed tasks up to max_attempts. Return completed tasks, permanently failed tasks, and blocked dependents separately.

Ask:

  • Does a lower priority number run first, or does a higher number win?
  • If a dependency permanently fails, should dependents be marked failed, skipped, or blocked?
  • Are retries immediate, delayed, or scheduled with backoff?

Represent the graph explicitly: dependents[task] lists tasks released by this task, and remaining[task] counts unmet dependencies. Use heapq for priority. The invariant is: a task enters the heap only when remaining[task] == 0. Three-state DFS (0 unseen, 1 active, 2 done) rejects a back edge to an active node in O(V + E) before run is ever called.

dependency-scheduler.py
1import heapq 2from collections import defaultdict 3from dataclasses import dataclass 4 5@dataclass(frozen=True) 6class Task: 7 name: str 8 priority: int 9 deps: tuple[str, ...] = () 10 11def schedule(tasks: list[Task], run, max_attempts: int = 2) -> tuple[list[str], list[str], list[str]]: 12 if max_attempts <= 0: 13 raise ValueError("max_attempts must be positive") 14 by_name = {task.name: task for task in tasks} 15 if len(by_name) != len(tasks): 16 raise ValueError("duplicate task name") 17 dependents: dict[str, list[str]] = defaultdict(list) 18 remaining = {task.name: len(task.deps) for task in tasks} 19 20 for task in tasks: 21 for dep in task.deps: 22 if dep not in by_name: 23 raise ValueError(f"unknown dependency: {dep}") 24 dependents[dep].append(task.name) 25 26 state: dict[str, int] = {} 27 28 def visit(name: str) -> None: 29 marker = state.get(name, 0) 30 if marker == 1: 31 raise ValueError("cycle") 32 if marker == 2: 33 return 34 state[name] = 1 35 for dep in by_name[name].deps: 36 visit(dep) 37 state[name] = 2 38 39 for name in by_name: 40 visit(name) 41 42 ready = [(task.priority, task.name) for task in tasks if remaining[task.name] == 0] 43 heapq.heapify(ready) 44 attempts = defaultdict(int) 45 completed: list[str] = [] 46 failed: list[str] = [] 47 48 while ready: 49 _, name = heapq.heappop(ready) 50 attempts[name] += 1 51 if not run(name): 52 if attempts[name] < max_attempts: 53 heapq.heappush(ready, (by_name[name].priority, name)) 54 else: 55 failed.append(name) 56 continue 57 58 completed.append(name) 59 for child in dependents[name]: 60 remaining[child] -= 1 61 if remaining[child] == 0: 62 heapq.heappush(ready, (by_name[child].priority, child)) 63 64 blocked = sorted(set(by_name) - set(completed) - set(failed)) 65 return completed, failed, blocked 66 67tasks = [ 68 Task("docs", priority=1), 69 Task("test", priority=2), 70 Task("deploy", priority=3, deps=("test",)), 71] 72fail_once = {"test"} 73 74def run_with_one_retry(name: str) -> bool: 75 if name in fail_once: 76 fail_once.remove(name) 77 return False 78 return True 79 80print(schedule(tasks, run_with_one_retry)) 81print(schedule([Task("test", 1), Task("deploy", 2, ("test",))], lambda _: False, max_attempts=1)) 82try: 83 schedule( 84 [Task("lint", 1, ("test",)), Task("test", 1, ("lint",))], 85 lambda _: True, 86 ) 87except ValueError as error: 88 print(error)
Output
1(['docs', 'test', 'deploy'], [], []) 2([], ['test'], ['deploy']) 3cycle

docs and test start ready. Priority 1 runs first, so docs completes. test fails once, retries, then succeeds and releases deploy. In the second call, test is permanently failed after one attempt, so deploy is blocked: it never ran. The cycle lint ↔ test is rejected before any run call, including for unrelated ready work.

Keep permanently failed tasks separate from blocked dependents. A blocked task never ran; callers may skip it, surface the failed prerequisite, or retry after repair.

Worker fanout

Keep graph construction and cycle detection single-threaded. Then protect only shared scheduler state: ready heap, attempts, completed, failed, and remaining dependency counts. Worker threads can run tasks outside the lock, then reacquire the lock to publish success or failure and release dependents.

If asked about retries, say whether retries preserve priority or use backoff. A concise full answer: ready tasks are claimed under a condition variable, execution happens outside the lock, and completion updates notify workers when new tasks become ready.

The crawler still needs a quota. That's the next drill: refill math with an injected clock, no sleep.

Token bucket with deterministic time

Rate limiters show up because they combine state, boundary conditions, and overload behavior. Retry storms get worse when every client wakes at the same backoff; jitter spreads those retries.[3]Reference 3Exponential Backoff And Jitterhttps://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ Inject now so the refill test can jump to 0.5s without sleeping.

The bucket below starts full. allow refills from elapsed time, then either consumes cost or returns how long the caller should wait. Impossible costs fail immediately instead of returning retry advice that can never succeed.

token-bucket.py
1import math 2from dataclasses import dataclass 3 4@dataclass 5class Bucket: 6 capacity: float 7 refill_per_second: float 8 tokens: float 9 updated_at: float 10 11class TokenBucketLimiter: 12 def __init__(self, capacity: int, refill_per_second: float) -> None: 13 if capacity <= 0: 14 raise ValueError("capacity must be positive") 15 if not math.isfinite(refill_per_second) or refill_per_second <= 0: 16 raise ValueError("refill_per_second must be positive and finite") 17 self.capacity = float(capacity) 18 self.refill_per_second = float(refill_per_second) 19 self._buckets: dict[str, Bucket] = {} 20 21 def allow(self, key: str, now: float, cost: float = 1.0) -> tuple[bool, float]: 22 if not math.isfinite(now): 23 raise ValueError("now must be finite") 24 if not math.isfinite(cost) or not 0 < cost <= self.capacity: 25 raise ValueError("cost must be greater than zero and no larger than capacity") 26 bucket = self._buckets.get(key) 27 if bucket is None: 28 bucket = Bucket(self.capacity, self.refill_per_second, self.capacity, now) 29 self._buckets[key] = bucket 30 if now < bucket.updated_at: 31 raise ValueError("now must not move backwards") 32 33 elapsed = max(0.0, now - bucket.updated_at) 34 bucket.tokens = min(bucket.capacity, bucket.tokens + elapsed * bucket.refill_per_second) 35 bucket.updated_at = now 36 37 if bucket.tokens >= cost: 38 bucket.tokens -= cost 39 return True, 0.0 40 41 missing = cost - bucket.tokens 42 retry_after = missing / bucket.refill_per_second 43 return False, retry_after 44 45limiter = TokenBucketLimiter(capacity=3, refill_per_second=1.0) 46print([limiter.allow("org-a", now=0.0)[0] for _ in range(4)]) 47print(limiter.allow("org-a", now=0.5)) 48print(limiter.allow("org-a", now=1.0)) 49try: 50 limiter.allow("org-a", now=2.0, cost=4.0) 51except ValueError as error: 52 print(error)
Output
1[True, True, True, False] 2(False, 0.5) 3(True, 0.0) 4cost must be greater than zero and no larger than capacity
Token counts after refill for a capacity-3 bucket that refills at 1 token per second. Three cost-1 allows at t=0 leave 0 tokens. The t=0 fourth request and the t=0.5 request stay below cost 1, so both deny. At t=1.0 the bar reaches 1.0 and the request is allowed.
After three allows at t=0 the bucket is empty, so t=0.5 still denies: only 0.5 tokens have refilled and retry-after is 0.5s. At t=1.0 the bar hits cost 1 and the request goes through.

The key can be a user, organization, endpoint, or model. Cleanup for idle buckets is a production memory concern, not a correctness requirement for the base prompt. A thread-safe version needs a lock around _buckets and bucket mutation.

Denied quota calls get retried. If the retry is a debit against an org's remaining credits, "run it again" is the wrong model. You need an idempotency key.

Credit ledger with idempotency

Keep credit-like state consistent: org token budgets, GPU-second reservations, or eval-run spend. Use append-only events when you can. If you also keep a balance, update the balance and the event together.

ledger-idempotency.py
1from dataclasses import dataclass 2 3@dataclass(frozen=True) 4class Event: 5 idempotency_key: str 6 account: str 7 delta: int 8 balance_after: int 9 10class Ledger: 11 def __init__(self) -> None: 12 self.balance: dict[str, int] = {} 13 self.events: list[Event] = [] 14 self.results: dict[str, Event] = {} 15 16 def apply(self, key: str, account: str, delta: int) -> int: 17 if key in self.results: 18 prior = self.results[key] 19 if (prior.account, prior.delta) != (account, delta): 20 raise ValueError("idempotency key reused with different operation") 21 return prior.balance_after 22 new_balance = self.balance.get(account, 0) + delta 23 if new_balance < 0: 24 raise ValueError("insufficient credits") 25 self.balance[account] = new_balance 26 event = Event(key, account, delta, new_balance) 27 self.events.append(event) 28 self.results[key] = event 29 return new_balance 30 31ledger = Ledger() 32print(ledger.apply("deposit-1", "org-a", 100)) 33print(ledger.apply("withdraw-1", "org-a", -30)) 34print(ledger.apply("deposit-1", "org-a", 100)) 35print(ledger.balance["org-a"], len(ledger.events)) 36try: 37 ledger.apply("deposit-1", "org-a", 200) 38except ValueError as error: 39 print(error)
Output
1100 270 3100 470 2 5idempotency key reused with different operation

A replay with the same key and operation returns the stored result, even if the org's balance changed later. Reusing a key for a different operation fails loudly instead of silently returning an unrelated balance. That's idempotency: the request key names the original effect, not "try the debit again."

Transfer follow-up:

  • Use one idempotency key for the whole transfer.
  • Lock account IDs in sorted order to avoid deadlock.
  • Record both debit and credit events together.
  • Define whether external side effects happen before or after durable commit.

A reusable lock-order helper makes deadlock prevention concrete:

ordered-account-locks.py
1from contextlib import ExitStack 2from threading import Lock 3 4locks = {"org-a": Lock(), "org-b": Lock()} 5 6def lock_accounts(*account_ids: str) -> ExitStack: 7 stack = ExitStack() 8 for account_id in sorted(set(account_ids)): 9 stack.enter_context(locks[account_id]) 10 return stack 11 12with lock_accounts("org-b", "org-a"): 13 print("locked:", sorted({"org-b", "org-a"}))
Output
1locked: ['org-a', 'org-b']

Crawlers, stores, schedulers, and ledgers are all mutable state plus an explicit claim. Parsers are the other common shape: an explicit current record and a policy for junk lines.

Multiline log parser

Group indented continuation lines under the previous valid event. Ignore orphan continuations and malformed records in this base version; a production parser should count or collect rejects.

multiline-log-parser.py
1import json 2 3def parse_events(text: str) -> list[dict[str, str]]: 4 events: list[dict[str, str]] = [] 5 current: dict[str, str] | None = None 6 7 for line in text.splitlines(): 8 if line.startswith(" "): 9 if current is not None: 10 current["message"] += f" {line.strip()}" 11 continue 12 13 parts = line.split("|", maxsplit=2) 14 if len(parts) != 3: 15 current = None 16 continue 17 18 timestamp, level, message = parts 19 current = {"timestamp": timestamp, "level": level, "message": message} 20 events.append(current) 21 22 return events 23 24LOGS = """ orphan continuation 252026-06-02T12:00:00Z|ERROR|request failed 26 timeout while calling model 27malformed line 282026-06-02T12:01:00Z|INFO|retry queued""" 29 30print(json.dumps(parse_events(LOGS), indent=2))
Output
1[ 2 { 3 "timestamp": "2026-06-02T12:00:00Z", 4 "level": "ERROR", 5 "message": "request failed timeout while calling model" 6 }, 7 { 8 "timestamp": "2026-06-02T12:01:00Z", 9 "level": "INFO", 10 "message": "retry queued" 11 } 12]

The leading indented line is dropped because there's no current event. The malformed non-indented line isn't an event and clears current, so a later indented line wouldn't attach to the ERROR record. Ask whether tabs count as continuation; this version only treats a leading space as a continuation.

Once the state model is right, interviewers often say "make it faster with threads." That follow-up is a concurrency-model question, not a request to sprinkle ThreadPoolExecutor on a CPU loop.

When threads help

Before you reach for threads or async, name what kind of work you're speeding up. That one sentence is a strong signal because it shows you understand why a concurrency model helps or doesn't.

On a standard GIL-enabled CPython build, only one thread executes Python bytecode at a time. Optional free-threaded CPython builds, available since 3.13, change that constraint, so state which runtime you're discussing.[4]Reference 4Python support for free threadinghttps://docs.python.org/3/howto/free-threading-python.html Free-threading isn't data-race freedom: shared mutable state still needs locks, atomics, or other synchronization. The official howto recommends threading.Lock instead of relying on the internal locks of built-in dicts and sets. Dropping locks because "there is no GIL" is still wrong.

Work typeExampleGIL effectRight tool
I/O-boundfetching URLs, reading disk, calling a model APIblocking I/O generally releases the GIL, so threads can overlap waitsthreads, ThreadPoolExecutor, or asyncio
CPU-bound Pythonheavy parsing or numeric loops in Python codethreads take turns under a GIL-enabled buildmultiprocessing or ProcessPoolExecutor

So the honest answer to "should we add threads?" is: only if the bottleneck is waiting, not computing. A crawler that spends its time on network round-trips is a good fit for threads. A function that spends its time crunching numbers in pure Python won't get faster with threads; it needs separate processes, or a library that drops into C and releases the GIL.

⚠️ Common mistake: Reaching for asyncio and then calling a blocking function inside a coroutine. Synchronous calls like requests.get(...) or time.sleep(...) don't yield to the event loop, so a single blocking line freezes every other task. The whole point of async is that each await hands control back so other work can run.

Each worker yields at await asyncio.sleep(0), so the event loop interleaves them:

asyncio-yield.py
1import asyncio 2 3async def worker(name: str, log: list[str]) -> None: 4 log.append(f"{name} start") 5 await asyncio.sleep(0) # yields control back to the event loop 6 log.append(f"{name} resume") 7 8async def main() -> list[str]: 9 log: list[str] = [] 10 await asyncio.gather(worker("a", log), worker("b", log)) 11 return log 12 13print(asyncio.run(main()))
Output
1['a start', 'b start', 'a resume', 'b resume']

Swap await asyncio.sleep(0) for a blocking time.sleep(0) and the output becomes ['a start', 'a resume', 'b start', 'b resume']: worker a runs start-to-finish before b ever begins, because a blocking call never returns control to the loop.

An interviewer asks you to make a log parser "faster with threads." The parser is pure-Python string processing over one large file. What do you say?

Answer

Say it's CPU-bound, so threads won't help under the GIL; they'll just take turns on one core. The real options are multiprocessing to use multiple cores, or dropping the hot loop into a C-backed library that releases the GIL. If the file were being fetched over the network, threads would help because that part is I/O-bound.

Before adding workers, say these five sentences:

  1. Shared state is X.
  2. The lock protects X.
  3. A work item is claimed at this point.
  4. Worker shutdown happens through this sentinel, event, or executor lifecycle.
  5. Failed work records an error and doesn't corrupt shared state.

Debugging when a test fails

Narrate the smallest useful investigation:

  1. Read the assertion and say expected versus actual.
  2. Reproduce with one smaller fixture.
  3. Print or inspect the state that owns the invariant.
  4. Fix the state transition, not the symptom.
  5. Add one regression test before moving to the next follow-up.
SymptomFirst thing to inspect
Duplicate outputclaim point or visited insertion timing
Missing outputenqueue condition, prefix filter, or failure policy
Wrong orderqueue/heap tie-break and where sorting happens
Expired key returnedread path skipped cleanup
Retry ran too many timesattempt counter increment point
Blocked task marked faileddependency failure policy mixed with task execution failure
Threaded result flakesshared state changed outside the lock

You don't have to be bug-free. Recover fast by naming which invariant broke: visited claimed too late, a TTL read skipped cleanup, or a lock dropped between compare and set.

When the interviewer adds a follow-up, classify it before changing code.

Follow-up typeFirst moveCommon trap
Timeinject now, store deadline, clean on readsleeping in tests
Orderingchoose deque, heap, sort, or insertion ordermixing policy into unrelated state
Capacityevict by explicit rulehidden off-by-one at capacity 0 or 1
Retrystore attempt count and final stateretrying external writes without idempotency
Snapshotversion records or copy-on-writemutating data a snapshot should freeze
Compare-and-setlock compare and write togethercalling get then set as two operations
Thread safetyprotect claim point and shared mapsholding lock during slow fetch or execution
Cancellationdurable flag plus cooperative checksstopping new work while workers keep publishing children

Pattern map

Most prompts are one of these shapes. Classify the prompt before writing code.

PatternRecognition signalFirst state modelHigh-value tests
TraversalURLs, graph nodes, dependencies, neighborsvisited plus queue or stackcycle, duplicate edge, malformed node, deterministic order
Mutable storeset, get, delete, scan, cache recordsdict from key to recordmissing key, overwrite, delete, scan ordering, expired key
Time boundaryTTL, deadline, rate limit, retry-afterinjected now, deadline fieldsequality boundary, refill edge, expired-on-read, no sleep
Ordering policypriority, deadline, LRU, LFU, ready queueheap, deque, OrderedDict, frequency bucketstie-break, stale heap entry, capacity 0, promotion
Idempotent writerequest ID, retry, ledger, external actionrequest key to stored resultduplicate success, duplicate failure, conflicting retry
Parserlogs, events, chunks, sections, streaming linesexplicit current record or buffermalformed line, continuation, empty input, final flush
Concurrencyworkers, thread-safe, fanout, cancellationlock-protected claim point plus queueduplicate claim, shutdown, exception, partial result
Validationschema, payload, permissions, filtersrecursive validator or rule tablenested failure, path reporting, unknown field, missing required

Many rounds feel like one product-shaped problem that grows across levels. Ship level 1 quickly, then preserve the same invariant.

Prompt familyLevel 1Level 2Level 3Level 4
GPU quota reservationadd, remove, lookupreserve/release capacityexpirations, over-allocation preventionconcurrency or audit log
Key/value storeset, get, deleteprefix scan and orderingTTL and compare-and-settransactions or snapshots
Crawlersame-host BFSURL normalizationretries and rate limitsworker fanout and cancellation
Schedulerdependencies and ready queuecycle detectionretries and deadlinesworkers, cancellation, fairness
Cacheget/put capacity evictionupdate semanticsTTL or LFUmetrics and thread safety
Chat or event routerregister handlersroute messagespriorities or filtersreplay, idempotency, backpressure
Log processorparse recordsmultiline eventstop errors and windowsmalformed input and streaming
Permission filterinclude/exclude resourcesgroups and inheritancedeny precedenceaudit why each item passed
Stream assemblerappend chunkssequence orderingduplicate/missing chunkstimeout and memory cap
Experiment splitterassign usersstable hashingramp percentagessticky overrides and rollback

State the tests before coding the follow-up. That keeps the interviewer aligned and prevents late rewrites.

PatternMinimum public testsPrivate-edge tests to rehearse
Crawler or graph traversalstart node, duplicate edge, off-host or invalid neighborcycles, relative links, malformed URL, empty graph, deterministic order
TTL store or rate limiterset/get, expiry, scan, refillequality boundary, zero TTL, time moving backward, cleanup during read
LRU or LFU cachecapacity eviction, update existing key, get promotioncapacity 0, tie-break by recency, expired entry, stale heap record
Schedulerindependent tasks, dependency release, retrycycle, missing dependency, permanent failure, blocked dependents
Ledgerdeposit, withdraw, transferduplicate idempotency key, conflicting retry, insufficient credits, lock order
Parserone record, multiline continuation, malformed lineorphan continuation, empty input, final flush, very large record
Validatorrequired field, unknown field, nested listerror path, default value, type coercion, repeated failure
Stream assemblerin-order chunks, out-of-order chunksduplicate chunk, missing end marker, timeout, memory cap

Practice circuit

Don't memorize wording. Learn the patterns. Each linked drill carries a full prompt, clarification questions, sample tests, hidden tests, a solution guide, and validated Python plus Java paths. Use Python first. The Java path is useful when you want maps, classes, and null checks to be explicit, not because labs require Java.

ArenaPractice drillsInterview signal
Repository and codebase systemsRepository Hash Tree, Repository Diff, Duplicate File Groups, Repository Ignore Filter, File Patch Applier, Patch Conflict Detector, Codebase Symbol Index, In-memory Filesystem, Stack Trace ReconstructorCode review agents, workspace snapshots, deterministic patches, symbol lookup, upload filtering, incremental state
Stateful servicesTTL Key/Value Store, Transactional KV Store, Idempotent Ledger, LRU TTL Cache, LFU TTL Cache, Token Budget LedgerDurable APIs, atomic updates, idempotency, eviction policy, TTL boundaries
Scheduling and workersDependency Scheduler With Retries, Workflow State Machine, Multi-tenant Job Scheduler, Bounded Worker Queue, Worker Lease Registry, Priority Deadline Scheduler, Batch Inference SchedulerRun orchestration, backpressure, retries, leases, fairness, cancellation follow-ups
Streaming and parsersStreaming Markdown Parser, SSE Event Parser, Streaming Token Assembler, Log Error Parser, Longest-match Tokenizer, Prompt Section Extractor, Secret RedactorChunked state, final flush, malformed records, overlapping matches, transcript safety
Agents and webhooksTool Call Schema Validator, Webhook Idempotency Receiver, PR Readiness Gate, Model Fallback Router, Retry Backoff Planner, Circuit Breaker StateTool execution, dedupe, provider fallback, proof artifacts, retry policy, outage behavior
Retrieval and contextRAG Chunk Selector, Context Window Packer, Document Permission FilterRanking, budget packing, access control, explainable inclusion
Evals and telemetryEval Failure Aggregator, Canary Metric Judge, Experiment Traffic Splitter, Agent Event Timeline, Command Log Classifier, Sliding Window Error CounterRollout judgment, failure clustering, reviewable proof, redacted logs, stable assignment, moving windows
Systems basics under frontier wordingSame-host Crawler, Token Bucket Rate Limiter, Notification Rate LimiterGraph traversal, duplicate suppression, host policy, quota math, noisy automation control

These drills are Python systems work: concurrency, state machines, caches, and ledgers. If a later panel also covers ML coding, reuse earlier lessons on tensor shapes, numerically stable softmax, attention, and training-loop invariants. The same claim-before-mutate discipline applies to buffer writes and gradient steps.

Common pitfalls

  • Solving version 1, then adding TTL, transactions, or threads without restating the invariant.
  • Using wall-clock sleeps in tests instead of injected time.
  • Making crawler output order part of correctness after adding concurrent workers.
  • Treating idempotency as "retry the operation" instead of recording the request key and result semantics.
  • Adding a global lock around everything without explaining throughput, deadlock, and fairness tradeoffs.
  • Stating a free-threaded runtime and then dropping locks on shared mutable state.
Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.Two crawler workers can see the same candidate URL while get_links is slow. Which claim protocol preserves duplicate suppression without serializing fetches?

Correct answer: Check and add the candidate to visited under one lock, then fetch outside the lock.

The check and insertion form one atomic claim. Doing it before fetch prevents two workers from starting the same request, while moving slow I/O outside the lock keeps workers concurrent.

2.A TTL key is written at time 10.0 with ttl 5.0, and expiry uses expires_at <= now. Which deterministic test checks the boundary?

Correct answer: Inject the clock, assert presence at 14.999, then advance it to 15.0 and assert the key is missing.

Injected time tests the exact contract without wall-clock delay or scheduler flakiness. Changing the comparison would hide the boundary instead of verifying the stated semantics.

3.A scheduler has docs at priority 1, test at priority 2, and deploy at priority 3 depending on test. Docs succeeds, test fails through two attempts, and deploy never runs. Which result separates failure from blockage?

Correct answer: Completed docs, failed test, and blocked deploy.

Docs is ready and succeeds. Test exhausts its attempts and becomes permanently failed. Deploy never ran because its dependency didn't complete, so it is blocked rather than failed.

4.A token bucket starts with capacity 3 and refill rate 1 token/second. Three requests consume it at time 0; a cost-1 request arrives at time 0.5. What result should it receive?

Correct answer: Denied with retry_after 0.5 seconds, because only half a token has refilled.

At 0.5 seconds the bucket has 0.5 tokens, less than the requested cost of 1. The deficit is 0.5 token at one token per second, so retry_after is 0.5 seconds.

5.A ledger already processed deposit-1 for 100, then processed a withdrawal for 30. A retry repeats deposit-1 with the same payload. What must remain unchanged?

Correct answer: The balance stays 70 and the event log stays at two entries while the stored result for deposit-1 is returned.

Idempotency maps a request key to its original result and prevents replaying the side effect. The current balance may have changed since the first request, but the duplicate doesn't append another event.

6.A log parser ignores orphan continuations, appends indented lines to the current event, and clears that event on malformed non-indented input. What follows an orphan, an ERROR plus continuation, a malformed line, and an INFO line?

Correct answer: Two events: ERROR with its continuation, then INFO; the orphan and malformed lines create no event.

An indented line without a current record is ignored. A valid header starts ERROR, its indented line extends it, malformed non-indented input clears the current state, and INFO starts the second event.

7.Two transfers can lock accounts in opposite directions. Which lock discipline prevents deadlock while keeping debit and credit atomic?

Correct answer: Acquire both account locks in sorted account-ID order, update balances and events, then release them.

A global order prevents one transfer from holding A while waiting for B and another holding B while waiting for A. Holding both locks through the state transition prevents a partially visible transfer; idempotency alone doesn't protect concurrent balances.

8.A dependency graph has A→B→C→A and an independent D. The scheduler must detect cycles before invoking run. Which validation is correct?

Correct answer: Use three-state DFS, reject on an edge to an active node, and invoke no task, including D.

An active-node back edge proves a cycle. Validating the complete graph before execution meets the requirement and avoids partial work, while a single visited set can't distinguish an active recursion path from a completed node.

8 questions remaining.

Next Step
Continue to AI Lab System Design Interview

The same claim points, quotas, idempotency keys, and failure states now have to be sized, turned into APIs, and operationally debugged as an end-to-end AI backend.

PreviousReasoning Agent System Design
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Interview guide

OpenAI · 2026

https://openai.com/interview-guide/

Careers

Frontier AI lab · 2026

https://www.anthropic.com/careers

Exponential Backoff And Jitter

Brooker, M. (AWS) · 2015

https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/

Python support for free threading

Python Software Foundation · 2026 · Python Documentation

https://docs.python.org/3/howto/free-threading-python.html

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.