Design GitHub Copilot
Inline AI code suggestions for 10 M developers, shown within 300 ms of a pause in typing, when most requests are never used.
Last updated 2026-10-27. Difficulty: hard. Patterns: latency-budget, cancellation, prefix-caching, context-assembly, code-completion. Reported at Microsoft, OpenAI, Anthropic, Google, Meta, Databricks.
The interviewer asks, the candidate answers and draws, and you press Next. Skip to any stage or key moment, and ask the AI Mentor anything along the way.
Or sit it in the style of Microsoft · OpenAI · Anthropic · Google, run as that company’s round is reported to go.
The short answer
How to design GitHub Copilot in a system design interview, in five steps:
- Scope it. Inline suggestions as the developer types, fill in the middle, relevant to the repository, typing through a suggestion, stale requests stop costing, organisation policies, sensible, safe output, quality measurement and model rollouts.
- Set the targets. Latency from the last keystroke to ghost text: p50 < 300 ms · p95 < 800 ms; Scale: 10 M daily · 3 M at peak · 600 k req/s; GPU efficiency: ≥ 80 % of prompt tokens from cache; Availability: 99.9 % · fail silent.
- Do the numbers. Requests per second at peak: ~600 k/s (750 k pauses/s); Requests that end in kept code: ~15 %; Prompt tokens to prefill: ~900 M/s raw · ~180 M/s after the cache.
- Draw the design. Context engine, Workspace index, Suggestion cache, Edge gateway, Org policy store, Region scheduler, Experiment config, Completion model, Prefix cache.
- Go deep where it is hard.
- Debounce, cancel and the typing-through cache → Adaptive debounce, a client gate and cache, cancellation propagated to the engine, prefill kept as cache.
- Context assembly under a token budget → A client-built fill-in-the-middle prompt: prefix, suffix, similar snippets from open tabs and recent edits, symbol signatures, packed by priority.
- Prefix caching and session affinity → A cache-friendly layout plus affinity by session key with bounded load.
- A latency budget measured in keystrokes → A small FIM model on one GPU, short max length, stop at end of line or block, speculative decoding, nearest region.
- Trimming and filtering what is shown → Trim to a syntactic unit, remove suffix overlap, check the parse.
- Acceptance, retention and model rollouts → A/B by user on accepted and retained characters per opportunity, with latency and filter guardrails.
Functional requirements
- Inline suggestions as the developer types. Grey ghost text at the cursor after a pause in typing, accepted with Tab (or word by word) and dismissed with Esc or by typing something else. A single line by default; a whole block when the cursor sits on an empty line that opens one.
- Fill in the middle. The suggestion fits between the code before and after the cursor: completing a call whose result the next line already uses, or a body whose closing brace already exists.
- Relevant to the repository. Context from open tabs, recently edited files and the definitions of symbols near the cursor, so suggestions use the project’s own names and helpers rather than generic ones.
- Typing through a suggestion. Typing characters that match the ghost text shrinks it without a new request; backspacing to a state already answered shows that suggestion again.
- Stale requests stop costing. A keystroke cancels the request in flight, and the cancellation reaches the GPU, so generation for a suggestion nobody will see stops within a step.
- Organisation policies. Per organisation: no retention of prompts or suggestions, content exclusion by repository and path, a filter that blocks suggestions matching public code, and which features are enabled.
- Sensible, safe output. Suggestions are trimmed to a syntactically sensible unit and filtered for secrets before they are shown.
- Quality measurement and model rollouts. Acceptance and code retained after acceptance, per model, language and IDE. New models ship through A/B experiments with automatic rollback.
- Out of scope. Chat and agents (a separate path to larger models; the ChatGPT question covers chat), training the models, the inference engine’s internals (the LLM Inference Service question), next-edit prediction, code review and billing.
Non-functional requirements
- Latency from the last keystroke to ghost text (p50 < 300 ms · p95 < 800 ms). The requirement behind the hardest trade-off. A suggestion that arrives after the next keystroke is wasted, so the budget is set by typing speed, and it forces a small model, nearby regions, short outputs and prefix caching. The server’s share is about 120 ms at p50.
- Scale (10 M daily · 3 M at peak · 600 k req/s). A very high request rate for an LLM product, but each request is small: about 1 500 prompt tokens and 24 output tokens. Prompt tokens, not output tokens, size the fleet.
- GPU efficiency (≥ 80 % of prompt tokens from cache). About 85 % of requests produce nothing the developer keeps, so the design is judged by GPU time per accepted character. A cancelled request must stop decoding within one step.
- Availability (99.9 % · fail silent). A failure is a missing suggestion, never an error dialog or a blocked editor. Clients do not retry a failed completion; the next pause is the retry.
- Privacy (no retention for business orgs). Prompts and suggestions are processed in memory and never logged, stored or used for training. Prefix caches are scoped to one session and never shared across users or organisations.
- Policy enforcement (server side within 1 min). Content exclusion is enforced on the client and again at the edge, so a stale or modified client cannot send excluded files. The public-code filter fails closed for organisations that require it.
- Client footprint (< 10 ms per request, off the UI thread). The extension does real work (indexing, snippet scoring, tokenising) and none of it may make typing lag. A slow keystroke costs more goodwill than a missing suggestion.
Back-of-envelope estimates
- Requests per second at peak: ~600 k/s (750 k pauses/s). 3 M active developers × one debounced pause every 4 s = 750 k pauses/s. The suggestion cache answers ~20 % of them on the client (typing through, backspacing to an answered state), so ~600 k requests/s reach the servers. That is ~20 B requests a day (~230 k/s average): a huge request rate, but every request is tiny.
- Requests that end in kept code: ~15 %. 40 % of requests are cancelled by the next keystroke (240 k/s). Of the 360 k/s that complete, ~15 % are empty or filtered, leaving ~300 k/s shown, and ~30 % of those are accepted: ~90 k/s. 0.6 × 0.85 × 0.3 ≈ 15 %, about one request in seven. Waste is the normal case, so the design is judged by what a wasted request costs.
- Prompt tokens to prefill: ~900 M/s raw · ~180 M/s after the cache. 600 k/s × 1 500 prompt tokens (prefix ~900, suffix ~200, snippets and signatures ~350, header ~50) = 900 M tokens/s, sixty times the output. With ~80 % of prompt tokens served from the prefix cache, ~180 M/s are actually computed. Unlike chat, prefill is the dominant GPU load.
- Output tokens per second: ~14 M/s run to the stop · ~10 M/s with cancellation. Line mode (75 % of requests) stops at the newline after ~12 tokens and block mode (25 %) after ~60, so ~24 on average: 600 k × 24 = 14.4 M tokens/s. Cancelled requests (40 %) that stop within a step of the cancel produce about a quarter of their tokens: 14.4 M × (0.6 + 0.4 × 0.25) ≈ 10 M/s. Propagating the cancel saves ~4 M tokens/s, about 400 GPUs.
- GPUs at peak: ~6 000 (vs ~25 000 naive). One GPU per replica with a small model (~8 GB of weights at 8 bits): ~50 k prompt tokens/s of prefill and ~10 k output tokens/s of decode at a batch of ~80 with 8 ms steps. 180 M ÷ 50 k = 3 600 plus 10 M ÷ 10 k = 1 000 is 4 600, ≈ 6 000 with 30 % headroom. Without the prefix cache and cancellation: 900 M ÷ 50 k + 14.4 M ÷ 10 k ≈ 19 400, ≈ 25 000 with headroom. Each point of prefix hit rate is worth ~180 GPUs.
- Warm-prefix memory per GPU: ~50 GB. With 32 layers, 8 KV heads of 128 dimensions and an 8-bit KV cache: 2 × 32 × 8 × 128 × 1 B = 64 KB per token, so a 1 500-token prompt is ~100 MB. 3 M developers ÷ 6 000 GPUs = 500 sessions per GPU, so ~50 GB. That fits in the ~64 GB an 80 GB GPU has left after 8 GB of weights and ~8 GB for the running batch, with host RAM as the overflow tier.
- Server share of the p50 budget: ~120 ms of ~240 ms. From the last keystroke: debounce 75, client context 10, client ↔ edge round trip 30, gateway 5, edge ↔ region 20, queue 10, prefill of ~300 uncached tokens 15, decode 60 (a line of ~14 tokens at 8 ms a step, roughly halved by speculative decoding), post-processing 10, render 5: 240 ms, leaving 60 ms of the 300 ms target. The server’s part, gateway to post-processing, is ~120 ms, half of it decode.
- Public-code index per region: ~800 GB. Fingerprint the ~1 T tokens of deduplicated training code as hashes of 20-token runs, keeping only the smallest hash in each window of 21 (winnowing): ~2 ÷ 22 ≈ 1 in 10 positions, which still catches any shared run of 40 tokens (~150 characters). 100 B fingerprints × 8 B ≈ 800 GB, sharded in RAM across a few hosts per region, or a ~100 GB Bloom filter in front of an SSD store.
Components
- Editor extension (VS Code · JetBrains · Neovim): Watches keystrokes, debounces them (about 75 ms, adapted to the developer’s rhythm) and decides whether to ask at all. Sends each request as one stream on a long-lived HTTP/2 connection and resets that stream on the next keystroke, which is where cancellation starts. Renders ghost text only if the document is still in the state the request was made for, and measures what happens to accepted text afterwards.
- Context engine (in the extension · token budget): Builds the prompt parts in under 10 ms off the UI thread: prefix and suffix around the cursor, snippets from open tabs and recent edits chosen by identifier similarity, and signatures of nearby symbols, packed by priority into a 2 048-token budget. Snippets are re-chosen when the cursor moves to a new region, not on every keystroke, which keeps prompts stable for the prefix cache.
- Workspace index (in memory · tabs + recent edits): An in-memory index of ~20-line windows from open tabs and recently edited files with their identifier sets, plus symbol signatures from the language server, updated incrementally as the developer edits. Files matching the organisation’s exclusion rules are never indexed, so they can never be chosen as context.
- Suggestion cache (in the extension · by doc state): Recent suggestions keyed by document state (a hash of the text around the cursor). Typing characters that match shown ghost text consumes it with no request, and backspacing to a state already answered shows its suggestion again. It answers about 20 % of pauses, and it is the only path where a suggestion costs no GPU at all.
- Edge gateway (HTTP/2 · auth · policy · quota): Runs in many points of presence close to developers and holds their long-lived HTTP/2 connections, so no request pays a handshake. Verifies a short-lived signed token locally, applies the organisation’s cached policy (exclusion re-checked on every path, retention, public-code filter), enforces per-user quotas and picks the nearest GPU region with headroom. Turns a client stream reset into a cancel toward the region.
- Org policy store (versioned document per org): Each organisation’s settings as one versioned document: retention off, content-exclusion patterns per repository, the public-code filter (block or allow) and enabled features. Edges cache it for 60 s, and every response carries the policy version, so a client holding an older one refreshes at once.
- Region scheduler (affinity · deadlines · arms): One per GPU region. Routes each request by its session key to the replica most likely to hold its prompt prefix, with bounded-load hashing so a hot replica spills to its neighbour, and resolves the user’s model arm from the experiment config. Answers a request that has waited past its deadline with an empty suggestion instead of serving it late, and forwards cancels to the replica.
- Experiment config (model arms · ramps · degrade): Maps (user hash, language) to a model arm with each arm’s ramp, and holds the degradation settings a region applies under load: debounce hint, block mode, draft length. Read on every request from a local copy, so a rollback or a degradation step is one config write that takes effect in seconds.
- Completion model (small FIM model · 1 GPU each): Replicas of a small fill-in-the-middle code model (~8 GB at 8 bits), one GPU each, continuously batched at about 80 sequences so a step stays near 8 ms. Renders the prompt suffix-first so typing only appends to it, applies the request’s stop rule (end of line or end of block) as it decodes, drafts tokens speculatively, and checks for cancellation at every step. One pool per model arm; chat runs on larger models in other pools.
- Prefix cache (KV blocks · HBM + host RAM): The attention state of each session’s last prompt, kept on its replica so the next request prefills only what changed; about 80 % of prompt tokens come from here. Roughly 100 MB per session and 500 sessions per GPU, with idle sessions moved to host RAM and back in a few milliseconds. Entries are scoped to one session key and never shared across users.
- Post-processor (trim · parse · filters): Turns raw output into something worth showing in about 10 ms: trims overlap with the text after the cursor, drops repeats of the previous line, rejects completions that add syntax errors and replaces strings shaped like credentials. Runs the public-code check for organisations that require it. Lives on the server so every IDE gets the same rules from one place.
- Public-code index (winnowed fingerprints · ~800 GB): Fingerprints of the deduplicated training code: hashes of 20-token runs, winnowed to about one position in ten, which still catches any shared run of 40 tokens (~150 characters). Sharded in RAM per region, about 1 ms per lookup, rebuilt and versioned with each training set. For organisations that block public-code matches, an unavailable index means no suggestion, never an unchecked one.
- Telemetry stream (Kafka · no code content): Client outcome events (shown, accepted, dismissed, retained at 30 s, 2 min and 5 min) and server timings per request: about 20 B client events a day, ~4 TB. Events carry ids, lengths, timings and similarity ratios, never code or paths, so the stream is the same for every customer and needs no exception for no-retention organisations.
- Quality pipeline (per arm · language · IDE): Joins events by suggestion id and computes the funnel per opportunity: sent, cancelled, shown, accepted, retained. Compares arms on accepted and retained characters per opportunity with latency and filter guardrails, and writes ramp or rollback decisions to the experiment config.
User flows
- A pause in typing becomes ghost text. The main path, inside about 300 ms from the last keystroke. Every hop is chosen for latency: the prompt is built on the developer’s machine, the request goes to the nearest region over a warm connection, lands on the replica that already holds its prefix, and stops at the end of the line.
- A pause in typing fires the debounce, and the extension checks its suggestion cache before asking. The debounce is about 75 ms, set from the developer’s own gaps between keystrokes and clamped to 50–150 ms. A cache hit (the document is in a state already answered) shows a suggestion with no request at all. A cheap local gate also skips positions where suggestions are rarely accepted, such as the middle of an identifier.
- The context engine assembles the prompt parts within the token budget. About 900 tokens of prefix and 200 of suffix around the cursor, plus ~350 tokens of snippets and symbol signatures that were chosen when the cursor entered this region; this step only reads them. It all runs off the UI thread in under 10 ms, so typing never lags. Excluded files are not in the index, so they cannot be picked.
- The extension sends the parts as one stream on its warm HTTP/2 connection to the nearest edge. The client sends parts, not a rendered prompt, so the server owns the model’s template and can change it with a model release. One connection per editor and one stream per request means a cancel is a stream reset, with no handshake and no new request. The session key lets the region route by affinity without the server keeping any session state.
- The edge verifies the token, applies the organisation’s policy and the quota, and picks a GPU region. The token is signed and carries the user, plan and organisation, so it is checked locally with no network call. The policy is cached for 60 s: every path in the request is re-checked against the exclusion rules, and the retention and public-code settings travel with the request. The region is the nearest one with headroom, from a capacity feed each region publishes every second. All of it costs about 5 ms.
- The region scheduler resolves the user’s model arm and routes by session key to the replica holding this file’s prefix. Bounded-load consistent hashing on the session key, the scheme in the ChatGPT question: the same document goes to the same replica unless that replica is above 1.25× the average load, in which case it spills to the next one and pays one cold prefill. The request carries a deadline, and one that has waited too long in the queue is answered empty rather than late.
- The replica prefills only the tokens after the cached prefix, then decodes with a draft until the line ends. The prompt is rendered suffix-first, so the typing since the last request only appended to it: here 1 456 of 1 480 tokens (91 full blocks) are already cached and prefill takes a few milliseconds. Line mode stops at the newline, checked as tokens are produced, so the GPU never decodes past the end of the statement. A drafter proposes a few tokens per step and the model verifies them in one pass.
- The post-processor trims the completion to fit its context and runs the filters. It drops a closing bracket that already follows the cursor, rejects a repeat of the previous line, parses prefix + completion + suffix to make sure no new syntax error appears, and replaces anything shaped like a credential. This organisation blocks public-code matches, so the completion plus the ~40 tokens before it are fingerprinted and checked against the regional index in about a millisecond.
- The suggestion returns through the edge and is rendered as ghost text if the document has not moved on. The client compares doc_version. If the developer typed in the last few milliseconds and the characters match the start of the completion, it shows the rest; otherwise it drops it. The suggestion goes into the cache keyed by document state. At p50 this is about 240 ms after the last keystroke, half of it on the server.
- Type through, accept, and measure what stayed. Much of what makes completions feel instant happens with no request at all: the client consumes a shown suggestion as the developer types it. Acceptance is only the start of measurement; what counts is whether the code is still there minutes later.
- Ghost text is on screen; once it has been visible long enough to read, the extension counts it as shown. A suggestion overtaken by typing within ~300 ms was never really seen, and counting it as shown and then rejected would make every fast typist look like a reluctant one. The suggestion id, model arm and time shown stay on the client until the suggestion is resolved.
- The developer types the first characters of the suggestion; the cache consumes them and the ghost text shrinks with no request. Each matching keystroke removes a character from the front of the ghost text and no debounce fires, so typing through costs nothing and feels instant. A keystroke that does not match dismisses the suggestion, and the normal path resumes at the next pause. This is most of the ~20 % of pauses the cache answers.
- The developer presses Tab; the text is inserted and an accepted event is queued for telemetry. Events are batched and sent every 30 s on the same connection. They describe the suggestion (id, arm, language, length, time shown, characters typed through) but never contain its text or the file path, so the stream is identical for every customer, with or without retention.
- At 30 s, 2 min and 5 min the extension measures how much of the accepted text is still there. The editor’s change events move the accepted range as the developer edits around it, and the extension computes the edit similarity between what was inserted and what that range holds now. Only the ratio leaves the machine. Acceptance counts a Tab; retention counts code that stayed, which is much closer to whether it helped.
- The quality pipeline joins the events by suggestion id and computes the funnel per opportunity for each arm. Per pause: sent, cancelled, shown, accepted, and characters retained at 5 minutes, sliced by arm, language and IDE. Per opportunity rather than per shown suggestion, because a model that shows fewer, safer suggestions raises its acceptance rate while helping less, and a slower model is shown less often.
- An arm that wins on retained characters with its latency guardrails held is ramped up in the experiment config. The ramp goes 1, 5, 25, 50 and 100 % over days and per language, because a model can be better overall and worse for one language. A guardrail breach (p95 latency, empty rate, filter hits) sets the arm back to 0 % automatically.
- Fast typing: cancel in flight, keep the prefix. A developer types a line in bursts, and most requests are overtaken by the next keystroke. The design has to stop paying for each one within a decode step and still reuse the prefill it already did.
- A 90 ms pause after “for item in” fires the debounce, and a request goes out. The developer is mid-line, typing in bursts with about 150 ms between keystrokes, so this request has a good chance of being overtaken before it returns. Sending it anyway is the bet the product makes: if the developer stops here, the suggestion is ready.
- The scheduler places it on the session’s replica, which prefills the new tokens and starts decoding. Prefill runs first and is short. The KV blocks it writes describe the prompt as it stands, which is exactly the prefix the next request will need, so this is the part of the request worth keeping whatever happens next.
- The developer types three more characters 110 ms later, and the extension resets the request’s stream. This is how 40 % of requests end. A stream reset is one small frame on a connection that is already open: nothing to authenticate, route or deduplicate, and it cannot race the original request down a different path.
- The edge turns the reset into a cancel, and the replica drops the sequence at the next step boundary. Each hop maps the cancelled stream to a cancelled context downstream, and the engine checks for cancellations between steps, so decoding stops within about 8 ms. Without this the replica would decode the rest of the line for nobody: across 240 k cancels a second, ~4 M tokens a second, or about 400 GPUs.
- The replica keeps r1’s prompt blocks as a cached prefix instead of freeing them. Cancelled work is not wasted work: the next prompt is r1’s prompt plus the characters just typed. If those characters happen to be what r1 had started to generate, even its generated tokens match the new prompt and are reused. Only blocks past the point where the two texts differ are freed.
- The next pause fires r2; it lands on the same replica and prefills eight tokens. Same session key, same replica, and a prompt that differs from r1’s only at its end. Time to the suggestion is then almost all decode, which is how a line can appear about 240 ms after the last keystroke even though every request is built from scratch on the client.
- At regional peak, responses carry a longer debounce hint, which removes requests before they exist. The cheapest request is the one never sent. Raising the debounce from about 75 to 150 ms at peak removes roughly a quarter of requests, mostly ones that would have been cancelled anyway, at the cost of suggestions appearing ~75 ms later. The hint comes from the region’s degradation config and lifts when load falls.
- A GPU region fails at peak. Losing a region removes an eighth of the capacity and every warm prefix in it at the worst moment. Its developers move to neighbouring regions with a longer round trip and cold caches, and the system sheds and degrades by deadline instead of queueing suggestions nobody will see.
- A GPU region stops publishing capacity, and the edges stop sending requests to it. Each region’s scheduler publishes free capacity and health every second, and an edge treats a region that misses three updates, or whose error rate spikes, as down. With eight regions this removes about 750 GPUs and the warm prefixes of ~375 k developers at once.
- Requests from those developers go to the next-nearest regions, with a longer round trip. Failover is per request, because nothing about a session lives anywhere a request must return to, so there is no state to migrate. The round trip grows by about 30 ms and p50 moves from ~240 to ~270 ms, still inside the target.
- Every moved session arrives with a cold prefix and pays one full prefill. 375 k sessions × 1 500 tokens is ~560 M tokens of extra prefill, most of it in the next ten seconds as each developer pauses: about a third more prefill in each receiving region for that window. After it, the steady extra load is ~15 % per region, inside the 30 % headroom.
- Schedulers answer requests that cannot meet their deadline with an empty suggestion instead of queueing them. A suggestion that arrives after the developer has moved on is worth nothing and still costs a full generation. Dropping at the head of the queue keeps the queue short, so the requests that are served are served in time. The client sees nothing, which is exactly what a late suggestion would have looked like anyway.
- If load stays high, the receiving regions step down a degradation ladder kept in their config. In order: raise the debounce hint (about a quarter fewer requests), switch block mode to line mode (block requests are a quarter of requests but ~60 % of decode tokens), shorten the draft if steps slow down, and finally shed free-plan traffic before business traffic. Each step is chosen for GPU time saved per point of quality lost, and each is undone automatically as headroom returns.
- Clients never retry a failed or empty completion; the next pause is the retry. A retry would arrive after its moment had passed and would double the load exactly when capacity is short. Errors are silent in the editor, and the status icon shows a degraded state only after many consecutive failures, so a regional incident looks like slightly fewer suggestions, not a broken tool.
- When the region recovers, the edges shift its traffic back gradually so its caches warm without a stampede. Moving all 375 k sessions back at once would repeat the cold-prefill spike in the other direction. The region’s weight ramps from 10 to 100 % over about 5 minutes, so each slice of sessions pays its one cold prefill while the rest stay warm where they are.
- An organisation excludes files and keeps nothing. Policy has to hold even when a client is old or wrong. Exclusion is enforced twice, on the client (excluded files are never read into context) and at the edge (every path in a request is checked), and no retention is a property of the whole request path, not a flag on a log line.
- An organisation admin adds exclusion rules and turns on the public-code filter; the policy version goes up. Policy is one versioned document per organisation, with exclusion patterns per repository. Business plans have no retention of prompts or suggestions by default, so that part needs no setting: it is how the serving path is built.
- Edges pick up version 43 within a minute, and each extension refreshes as soon as a response shows the version changed. Edges cache each organisation’s policy for 60 s. Every completion response carries the policy version, so a client holding version 42 refetches on its next request rather than waiting for its 10-minute poll.
- The extension drops excluded files from its index and makes no request while the cursor is in one. This is the first line of defence and the strongest one: text that is never read into context cannot be sent. Rules match repository-relative paths, so the same rule works in every IDE and on every machine.
- The edge re-checks every path in each request against the rules and strips or refuses what is excluded. An old or modified client could still send excluded text, so the server enforces the rule too: snippets tagged with an excluded path are removed, and a request whose current file is excluded is refused. The server can only check the paths a client reports, so organisations with exclusion rules refuse clients older than the version that supports them.
- The request is processed in memory only: nothing about its content is logged or stored. Logs carry ids, sizes and timings, never text or paths. Prefix-cache blocks live in GPU or host RAM, are scoped to the session key and are evicted after about 10 idle minutes, and core dumps on GPU hosts are disabled. No retention is a property of every component on the path, because one debug log line that prints prompts would break it.
- The post-processor suppresses any suggestion that matches public code for this organisation. The check covers the completion plus the ~40 tokens before it, since a short completion can extend a public passage the developer started typing. A match means nothing is shown, not a trimmed variant. If the index is unreachable, this organisation gets no suggestion rather than an unchecked one.
- The organisation’s usage and filter counts reach its admin dashboard through the same code-free telemetry. Admins see active users, acceptance and how often the public-code filter fired, aggregated per organisation. Because events never contain code, the analytics path needs no exception for no-retention customers.
Deep dives
Debounce, cancel and the typing-through cache
A developer types five characters a second. Why not ask for a completion on every keystroke, and what stops most of your GPU time going to suggestions nobody sees?
If 3 M active developers typed continuously at five characters a second, a request per keystroke would be 15 M requests a second, twenty-five times the 600 k planned, and nearly all of it stale on arrival: a response takes ~150 ms of network and server time, and the next keystroke comes in ~200 ms. Even with debouncing, about 40 % of requests are overtaken by the next keystroke, and of the suggestions that are shown only ~30 % are accepted, so about one request in seven ends in code the developer keeps.
The waste comes in three layers, each with its own fix. Some requests should never be sent: debounce, a cheap client gate and the typing-through cache remove them. Some become stale while in flight: cancellation has to reach the GPU within a step, or it is only a client-side discard. And some of the work done for a stale request is still useful: its prefill is the next request’s prefix. The goal is not to stop speculating, since speculation is the product, but to stop paying for a bet the moment it has lost.
- A request on every keystroke, stale responses dropped on the client rejected
- Fixed debounce with client-side discard only rejected
- Adaptive debounce, a client gate and cache, cancellation propagated to the engine, prefill kept as cache chosen
- Prefetch the next completion while one is shown situational: Right after an accepted suggestion, when the next position is predictable, and only while the region has headroom; it is the first thing switched off under load.
The answer: The extension debounces with a delay set from the developer’s own rhythm (around the 75th percentile of their gaps between keystrokes, clamped to 50–150 ms), and a cheap local gate skips positions where suggestions are rarely accepted, such as the middle of an identifier or a spot where a suggestion was just dismissed. Before sending, it checks the suggestion cache keyed by document state: typing the characters of shown ghost text consumes it, backspacing to an answered state shows it again, and about 20 % of pauses end there. Each request is one stream on a long-lived HTTP/2 connection; the next keystroke resets that stream, the edge and the scheduler propagate the cancel as a cancelled context, and the engine checks for cancellations at every step boundary, so decode stops within ~8 ms. The engine keeps the cancelled request’s prompt blocks as a cached prefix, so the request a keystroke later prefills only a few tokens. At peak, propagation saves ~4 M decode tokens a second (about 400 GPUs), and the reuse is a large part of why the prefix hit rate holds near 80 %. The numbers to watch are cancels per request, tokens decoded after a cancel arrived, and pauses answered by the cache.
The response arrives 5 ms after the developer typed another character. What does the client do?
Compare versions: the request was for document version v and the editor is now at v + 1. If the typed character matches the start of the completion, show the rest, which is the typing-through rule applied to a response that raced the keystroke. Otherwise drop it and let the debounce for v + 1 fire. Never render a suggestion computed for a document state that no longer exists; ghost text that disagrees with the code is worse than none.
Why is an HTTP/2 stream reset better than a separate cancel request?
It is free and immediate: one small frame on a connection that is already open, nothing to authenticate or route, and every proxy that speaks HTTP/2 or gRPC turns it into a cancelled context downstream. A separate cancel call could race the original on a different path, reach a different edge, and need its own idempotency. The cost is that every hop must actually honour cancellation: one proxy that buffers, or an engine that only checks between batches, silently turns cancellation back into a client-side discard.
How do you choose the debounce?
As an experiment rather than a constant: arms at 50, 75, 100 and 150 ms and the adaptive rule, compared on accepted characters per opportunity, end-to-end latency and GPU tokens per accepted character. A shorter debounce shows suggestions sooner but cancels more; a longer one saves GPU but feels sluggish. The adaptive rule usually wins because typing rhythm varies several times between people: a fixed 75 ms means constant cancellations for a fast typist and a lag for a slow one.
Ten times the developers. What changes in this layer?
Nothing breaks here, because debounce, gate and cache scale with the developers’ own machines. What grows is the GPU fleet, and the client is where the cheapest savings are: a gate or cache that is 10 % better removes 10 % of requests before any GPU sees them. At that scale I would train the gate as a tiny on-device model on acceptance data, and spend effort on the client before the server.
How do you know cancellation actually reaches the GPU?
Measure the gap. For every cancelled request the engine logs the tokens decoded after the cancel arrived and the time from the client’s reset to the engine’s abort; both should be about one step, 8 ms and zero or one token. A rising number means some hop stopped propagating, such as a proxy upgrade that buffers or an engine change that checks less often. That is a GPU-cost regression users never notice, which is exactly why it needs its own alarm.
Context assembly under a token budget
The model sees about 1 500 tokens. Which 1 500, who chooses them, and how do you make suggestions repository-aware without breaking the latency budget?
The file being edited is often thousands of lines long, the definitions it uses live in other files, and the code after the cursor matters as much as the code before it: completing a call is easier when the next line already uses its result. But every prompt token is paid in prefill on every request, 600 k times a second, and assembly has to fit in about 10 ms on the developer’s machine. Bigger prompts help suggestions up to a point, and their cost in latency and GPUs grows linearly the whole way.
Two observations shape the answer. Fill-in-the-middle training (Bavarian et al., 2022) lets a model condition on both the prefix and the suffix with little loss of its ordinary left-to-right ability, so the suffix belongs in the prompt. And the most useful extra context is usually close at hand: the files the developer has open or just edited, and the definitions of symbols near the cursor. GitHub has written publicly about using snippets from neighbouring tabs; the developer’s machine already holds all of that in memory, and the server does not.
- Prefix only, completed left to right rejected
- A client-built fill-in-the-middle prompt: prefix, suffix, similar snippets from open tabs and recent edits, symbol signatures, packed by priority chosen
- A server-side repository index with embedding retrieval situational: Chat and agent features, which tolerate more latency, or very large monorepos, with retrieval run when the cursor moves to a new area rather than on every keystroke.
- Send whole files up to a large context window rejected
The answer: The context engine in the extension keeps a workspace index of ~20-line windows from open tabs and recently edited files of the same language, with their identifier sets, and gets signatures of symbols near the cursor from the language server. When the cursor enters a new region (a new function, a jump, another file) it scores the windows by Jaccard similarity of identifiers against the ~20 lines before the cursor and keeps the best, off the typing path; GitHub’s exact ranking is not public, so this is my design. Each request packs a 2 048-token budget by priority: the ~900 tokens of prefix nearest the cursor, then ~200 of suffix, then signatures, then snippets, then more distant prefix, each snippet tagged with its path. The client sends the parts, not a rendered prompt, so the server renders the model’s template and can change it with a model release. Excluded files never enter the index. Priority decides what gets in; the server’s layout decides the order, which matters for caching.
Why Jaccard similarity of identifiers rather than embeddings?
It is cheap enough to run on every cursor move inside the IDE over a few thousand windows in a millisecond or two, needs no model on the client, and code that shares identifiers is usually the code that helps: the same API called the same way. Embeddings capture more meaning, but they need a model and an index, and their gain for a single-line completion is smaller than for chat retrieval. I would run embeddings as an experiment arm against Jaccard on accepted characters per opportunity rather than assume either way.
An organisation excludes secrets/** and a developer has secrets/keys.py open in a tab. Walk me through it.
The context engine never indexes that file, so none of its windows can be chosen as a snippet, and no request is made while the cursor is in it. If an old or modified client sent a snippet tagged with that path anyway, the edge strips it, and a request whose current file is excluded is refused. What neither layer can catch is text copied from that file into a file that is not excluded: exclusion governs files, and the documentation should say so plainly.
How do you count tokens on the client when the tokenizer belongs to the model?
Ship the tokenizer with the extension, since BPE tables run fine in process, versioned with the model family, and have the server return the tokenizer version it expects. Count with a small safety margin. If the server still finds a prompt over budget, say after a model change, it trims the far end of the prefix at a line boundary rather than failing the request.
A function’s definition is in a file that is not open. Is it missed?
Mostly not: the language server resolves symbols near the cursor wherever they are defined, and the engine includes their signatures, which is most of what a completion needs. The bodies of unopened files are missed, and that is the case for a server-side index. I would add one only for repositories where offline evaluation shows a gain, and run its retrieval on cursor moves so it is never on the keystroke path.
How would you know a context change helps?
Offline first: replay held-out edits from internal repositories, mask the code that was actually written, and compare exact match and edit similarity with the old and new context. Then online as an A/B arm on accepted and retained characters per opportunity, with prompt tokens and latency as guardrails, because more context that helps a little but adds 20 % to prefill may not be worth its GPUs.
Prefix caching and session affinity
Consecutive requests from one developer share about 97 % of their prompt. How do you stop paying to prefill it again 600 thousand times a second?
Prefill dominates this workload: 600 k requests a second × 1 500 tokens is 900 M prompt tokens a second against 14 M output tokens, so uncached prefill alone would need about 18 000 GPUs. Yet the request a keystroke after the last one has the same header, snippets, suffix and prefix except for the few characters just typed. The engine can reuse KV blocks for an exact token prefix it already holds (paged KV and prefix reuse are covered in the LLM Inference Service question), but only on the replica that holds them, and only if the change is at the end.
So the product decides the hit rate twice. The prompt must be laid out so typing only appends to it, and consecutive requests must reach the same replica. The obvious layouts put the changing part in the middle: in the common prefix-suffix-middle order the suffix comes after the text being typed, and a prefix window that slides down one line per line typed changes the start of the prompt on every new line.
- Least-loaded routing with any layout rejected
- A cache-friendly layout plus affinity by session key with bounded load chosen
- A server-side session: the client sends edits as deltas and the server keeps the document situational: Cloud IDEs that already run the document on a server, or clients on very constrained links.
- Pin each developer to one replica for the day rejected
The answer: The server renders prompts suffix first: header, snippets and signatures, then the suffix, then the prefix, then the marker for the middle, so text typed at the cursor only appends to the prompt. The FIM paper trains and evaluates both the prefix-first and suffix-first orders, so this is a training choice rather than a hack. Snippets are sticky (re-chosen only when the cursor moves to a new region, in a deterministic order), and the prefix window’s start snaps to a 32-line boundary instead of sliding a line at a time, trading slightly longer prompts for a start that rarely moves. The region scheduler hashes the session key, a hash of (installation, document), onto replicas with bounded-load consistent hashing capped at 1.25× the average, the scheme from the ChatGPT question; a spill costs one cold prefill. Idle sessions’ blocks move from GPU memory to host RAM and back in a few milliseconds, and every entry is scoped to its session key. The result is a hit rate around 80 % and a fleet of ~6 000 GPUs instead of ~25 000.
Why key affinity by document rather than by developer?
Consecutive requests share a prefix only within one document; a developer who switches files gets a different header, snippets and text, so keeping them on one replica buys nothing and concentrates a heavy user’s load. Keying by document spreads one developer across replicas and lets each hold one file’s prefix. When the developer switches back to a file within a few minutes, its prefix is often still warm on that file’s replica.
A replica dies. What does it cost?
Its ~500 sessions rehash to their next replicas on the ring, with the load bound spreading them out, and each pays one full prefill of ~1 500 tokens on its next request: about 35 ms instead of 15, once. In total that is ~750 k tokens, about fifteen GPU-seconds of prefill spread over several neighbours, which nobody notices. Losing a whole region is the expensive version, because it moves hundreds of thousands of sessions at once.
Does putting the suffix first hurt quality?
It is a property of training, not of serving: the model has to be trained with the suffix-first format, and then evaluation decides. If it showed a loss, the fallback is the prefix-first order with a short suffix, accepting that the suffix and everything after the change are recomputed each time: a few hundred tokens per request rather than the whole prompt, so a lower but still useful hit rate.
How much memory do warm prefixes need, and what happens when it runs out?
About 100 MB per session (1 500 tokens × 64 KB with an 8-bit KV cache) and ~500 sessions per GPU, so ~50 GB, which fits beside 8 GB of weights and ~8 GB for the running batch on an 80 GB GPU. Beyond that, idle sessions move to host RAM, where a host has a terabyte or more, and come back over PCIe in a few milliseconds. Eviction is least recently used; a developer idle for ten minutes loses the prefix and pays one cold prefill.
Could two developers share cache entries, say for a popular open-source file?
The bytes might be identical, but sharing across users creates a timing side channel (a fast response reveals that someone else had that exact prompt), and a business customer’s code must not influence another tenant’s requests in any way. The only safely shareable part is the template’s fixed tokens, which are tiny. So entries are scoped to the session key and cross-session reuse is limited to those fixed tokens.
A latency budget measured in keystrokes
Your best model writes better code. Why not use it for inline completions?
The budget is set by typing. A fluent typist leaves about 200 ms between keystrokes, so a suggestion that arrives 600 ms after a pause usually lands in code that has already changed, and it is cancelled or never seen. From the last keystroke, a p50 of 300 ms has to cover a 75 ms debounce, client work, two network hops, queueing, prefill, decode, filtering and rendering. A large chat model on a multi-GPU replica might take 30–50 ms per token, so a 16-token line would be 500–800 ms of decode alone.
Inline completions are short and most are discarded, which inverts the usual quality trade-off: a slightly weaker suggestion shown at 200 ms is worth more than a better one at 700 ms, because acceptance depends on being on screen at the right moment. So the budget is split per step, each step gets a number, and the model is chosen to fit the decode share, not the other way round. Chat stays on a separate path with its own pools and larger models, where seconds are acceptable.
- The large chat model for everything rejected
- A small FIM model on one GPU, short max length, stop at end of line or block, speculative decoding, nearest region chosen
- A cascade: the small model first, a large model for blocks on long pauses situational: Explicit requests or pauses over a second at the start of a block, where an answer at one second still arrives before the developer moves on.
- Run the model on the developer’s machine situational: Offline or air-gapped environments, or as the gate and draft model rather than the main model.
The answer: The p50 budget from the last keystroke: debounce 75 ms, context on the client 10, client ↔ edge 30, gateway 5, edge ↔ region 20, queue 10, prefill 15, decode 60, post-processing 10, render 5, which is 240 ms with 60 ms of slack, and the server’s share is ~120 ms. The model is a small FIM-trained code model (~8 GB at 8 bits) on one GPU per replica, with batches held near 80 sequences so a step stays around 8 ms. Each request carries a mode: line (stop at the newline, at most 64 tokens) or block (on an empty line that opens a block; stop when the indentation returns to the block’s level or the brackets balance, at most 256), and the engine checks the stop rule as tokens are produced so the GPU stops early. Speculative decoding drafts a few tokens per step with a tiny draft model or with n-grams copied from the prompt, since code repeats its own identifiers, roughly halving the steps on code; the draft length is lowered as batches grow, because verification then competes for compute, and the 60 ms of slack covers losing it at peak. There is no streaming to the client: a line is ~12 tokens and fits in one small response. Every hop’s time is logged per request so a p95 regression can be pinned to a step.
If p50 is 240 ms, where does a p95 of 800 ms come from?
Mostly four places: block completions (~60 tokens, ~250 ms of decode), cold prefixes after a file switch or a replica move (a full 1 500-token prefill in a busy batch), queueing at peak, and the developer’s own network (hotel Wi-Fi, a VPN hairpin, 150 ms or more of round trip). Each has a different fix, which is why per-step timings exist: capacity for queueing, sticky snippets and affinity for cold prefixes, nearer regions for the network, and nothing at all for blocks if 800 ms is acceptable for them.
Why one GPU per replica rather than a big tensor-parallel replica?
The model fits in 8 GB, so tensor parallelism would only add communication to every step with no memory benefit, and it would make each replica a bigger failure domain. With one GPU per replica there are about 6 000 small, identical units, bounded-load hashing across them is simple, and losing one touches ~500 sessions rather than thousands.
How does speculative decoding work here, and when would you turn it off?
A cheap drafter proposes the next few tokens, either a tiny model or a lookup of n-grams that already appear in the prompt, which works well on code because names repeat, and the main model verifies them all in one step, keeping the longest correct run, so the output is unchanged. It cuts steps per suggestion, which is latency. At large batch sizes verifying several tokens per sequence makes each step compute-bound and slower, so the scheduler shortens the draft as batches grow and turns it off if step time rises.
Isn’t stopping at the end of the line too conservative?
Mid-line it is what the developer wants: they are finishing a statement. Block mode covers the case where a whole body is useful, and it is triggered by position (an empty line after a signature or a block opener) rather than by default, because a 60-token suggestion takes five times the decode and is accepted less often per token. The split is itself an experiment: the share of block mode and its retained characters decide whether to widen it.
What would you change at ten times the developers?
The per-request design holds and the fleet grows to tens of thousands of GPUs, so efficiency compounds: a better client gate, a higher prefix hit rate (each point is ~180 GPUs today), 4-bit weights where quality allows, and distilled per-language models where a language’s volume justifies one. And more regions closer to developers, since network is about 50 ms of the 240 ms p50 and only geography fixes it.
Trimming and filtering what is shown
The model returned 40 tokens. What do you actually show, and how do you make sure it is not someone else’s code or a leaked key?
Raw model output is rarely the right thing to insert. It may run past the end of the statement, repeat the closing bracket that already follows the cursor, loop on the previous line, or be syntactically broken in the context it lands in. Separately, a model trained on public code can reproduce long passages of it verbatim, which some organisations must not accept, and it can emit strings that look like credentials. All of this has to run in about 10 ms on 360 k completions a second.
Two observations make it tractable. Truncation works best during generation as well as after it: stop rules in the engine save GPU time, and a final trim makes the text fit its context. And matching against public code does not need a search over all of it: it is a set-membership question about fixed-length runs of tokens, the classic fingerprinting problem, so a hashed and sampled index answers it in about a millisecond.
- Show raw output and let the developer edit rejected
- Trim to a syntactic unit, remove suffix overlap, check the parse chosen
- A public-code filter on winnowed fingerprints in a regional index chosen
- A model-based security reviewer on every suggestion situational: As a fast classifier on block completions only, or asynchronously as a warning on accepted code rather than a filter in the request path.
The answer: The engine applies the mode’s stop rule while it decodes, so most outputs already end where they should. The post-processor then trims overlap with the suffix (a completion ending in the “)” that already follows the cursor loses it), drops a completion that repeats the previous line, trims trailing whitespace, and parses prefix + completion + suffix with an incremental parser, rejecting a completion that adds syntax errors. For the public-code filter, the index holds fingerprints of the deduplicated training set: a hash of every run of 20 tokens, keeping only the smallest hash in each window of 21 (winnowing), which stores about one position in ten and guarantees catching any shared run of 40 tokens, roughly 150 characters. A suggestion is checked together with the ~40 tokens of prefix before it, and any match means nothing is shown for organisations that block matches. Secrets are caught by patterns for known credential formats plus an entropy check on string literals, and a hit replaces the literal with a placeholder or drops the suggestion. The rules and the index are versioned with the model they protect, and the rules live on the server so every IDE changes at once.
Why check the context along with the completion?
Because a 13-token completion cannot match 40 tokens of anything by itself, yet it can be the continuation of a public passage the developer is retyping or pasted the start of. Checking the window that spans the end of the prefix and the completion catches a suggestion that would extend a verbatim copy. The cost is false positives on boilerplate everyone writes, which is why the window is long and the index is built from deduplicated training data.
The index is 800 GB. Where does it live, and what if it is down?
Sharded by fingerprint across a few memory-optimised hosts per region, or as a ~100 GB Bloom filter on each post-processing host with exact checks only on Bloom hits, which are rare. If it is unavailable, organisations that block matches get no suggestions (fail closed) rather than unfiltered ones, because the setting is a promise; everyone else is unaffected. It is rebuilt offline whenever the training set changes.
How do you trim to a sensible unit in a language you cannot parse?
Fall back to generic rules: stop at the newline in line mode, and in block mode track bracket balance and indentation relative to the line where the block started, which works for most C-family and indentation-based languages. The parse check is skipped where there is no grammar. Because the rules live on the server, adding a grammar improves every IDE at once.
A suggestion contains what looks like an AWS key. Where did it come from, and what do you do?
Either it was memorised from training data or it was copied from the developer’s own context, such as a key in an open file. Either way it should not be suggested: known formats and high-entropy literals are replaced with a placeholder or the suggestion is dropped. Prevention is upstream too: the context engine skips files such as .env and key files by default, and training data is scrubbed of secrets. Counting hits by source tells you which upstream fix matters most.
How do you know the filters are not hiding good suggestions?
Log how often each filter fires and at what kind of position, and run holdbacks where a small share of traffic has a non-policy filter (a trimming rule) relaxed, comparing retained characters. The public-code and secret filters are promises, so they are never relaxed online; their false positives are measured offline on labelled samples.
Acceptance, retention and model rollouts
You trained a new completion model. How do you know it is better, and how do you ship it to 10 million developers without making their editors worse?
Offline benchmarks, such as infilling tasks of the kind the FIM paper introduced and held-out repositories, say whether a model can write code, not whether developers keep what it shows in their own code at the latency it runs at. Online, a raw acceptance rate is easy to move the wrong way: a model that shows fewer, safer suggestions has a higher acceptance rate and helps less, and a slower model is shown less often because the developer typed on. And a Tab press is not success: accepted code that is deleted a minute later was a distraction.
So the measurement has to be per opportunity, include latency, and look past the Tab: accepted characters per pause, and the share of accepted characters still present minutes later. With ~300 k suggestions shown a second, statistical power is not the problem; choosing metrics that cannot be gamed, and guardrails that catch regressions in latency and filter hits before a model reaches everyone, is. GitHub has published research (Ziegler et al., 2022) finding acceptance rate a good predictor of developers’ perceived productivity, which is a reason to watch it, though not alone.
- Offline benchmarks only situational: As the gate before any online exposure, never as the ship decision.
- Acceptance rate per shown suggestion as the ship metric rejected
- A/B by user on accepted and retained characters per opportunity, with latency and filter guardrails chosen
- Shadow traffic: run the new model on live requests without showing it situational: Before a new model’s first user-facing arm, on about 1 % of requests, to check its latency and GPU cost on real prompts.
The answer: A new model first passes offline infilling and held-out-repository evaluations, then shadows ~1 % of requests to measure latency and GPU cost on real prompts, then enters an A/B experiment: users are assigned by hashing their id with the experiment’s salt (the A/B Testing Platform question covers the mechanics), so each developer sees one model consistently, and each arm gets its own replica pool because prefix caches are per model. The primary metric is accepted characters per opportunity and the decision metric is characters still present at 5 minutes per opportunity, measured on the client as edit similarity over the accepted range and sent only as numbers. Guardrails are p50 and p95 latency, empty and filtered rates, cancel rate and errors, sliced by language and IDE, and a breach sets the arm to 0 % in the experiment config, which the scheduler reads on every request, so rollback takes seconds. The ramp is 1, 5, 25, 50 and 100 % per language over days, and the old model’s replicas stay warm until the new one has held at 100 % for several days.
Why measure retention on the client rather than the server?
Only the editor sees what happens to the code after it is inserted: edits, deletions, undo. The server sees nothing until the file is committed, if ever, and for no-retention organisations it must not see the code at all. The extension follows the inserted range through change events and sends a similarity ratio at fixed times, so the measurement needs no code to leave the machine.
Faster models get accepted more. How do you separate quality from speed?
Decide on the combination developers actually get, because that is the product, and per-opportunity metrics fold latency in naturally. To understand why an arm wins, slice by latency bucket: comparing suggestions shown at the same latency isolates quality. Sometimes the right decision is the faster, slightly weaker model, and the metric should allow that.
A new model is better overall but worse for one language. Ship it?
Not as one decision. The experiment config maps language to arm, so ship the new model where it wins and keep the old one where it loses while the training team looks at why. Language is a first-class slice in the guardrails, because a regression in a smaller language disappears inside a global average.
How do you roll back in the middle of the day?
One write to the experiment config, which maps (user hash, language) to an arm and is read by the scheduler on every request, so it takes effect within seconds. The old model’s replicas are kept warm until the new one has held at 100 % for days, so there is capacity to roll back to. Nothing needs draining, since each request lives about a hundred milliseconds.
What is a sign the metric is being gamed?
Acceptance rising while characters per opportunity or retention fall, which means the model is showing shorter, safer suggestions. Or suggestions shown per pause falling, which means it is slower or returns empty more often. Watching the whole funnel per opportunity (sent, cancelled, shown, accepted, retained) rather than one ratio makes those shifts visible.
The theory behind it
- Serving LLMs on GPUs: The mechanics behind fast, affordable model serving: prefill and decode phases, the KV cache and paged attention, continuous batching, quantisation, tensor and pipeline parallelism, speculative decoding, prefix caching, routing and autoscaling GPU fleets, and the metrics that matter.
- Tail latency, percentiles and queueing: Why p99 matters more than the average, how fan-out amplifies tail latency, Little's law and queueing near full utilisation, hedged requests, timeouts and deadlines, and how to set and measure latency budgets.
- Building LLM features: serving, latency and cost: System design for LLM-powered products: streaming responses, the cost and latency of tokens, batching and KV caches in inference, prompt caching, rate limits and fallbacks, guardrails, evaluation, and when to use RAG, tools or fine-tuning.