API and real-time communication interview questions
How services and clients talk: choosing REST, GraphQL, or gRPC for a scenario, idempotent and evolvable APIs, pagination, push transports from polling to WebSockets, scaling and resuming real-time connections, webhooks, deadlines and retries across layers, and a real-time order tracking design prompt.
Answer out loud or on paper before revealing. If your answer misses a key point, revisit the linked guide or explainer.
Nothing is due right now. Every question in this set is scheduled for a later review.
You are building a public partner API, a mobile app with several screens of nested data, and internal service-to-service calls at high volume. Which API style would you pick for each, and why?
Reveal model answer
Public partner API: REST over HTTP with JSON, because every language and tool speaks it, HTTP caching and status codes work out of the box, and resource URLs are easy to document, version, and rate limit. Mobile app with nested, screen-shaped data: GraphQL (often behind a BFF), because the client asks for exactly the fields a screen needs in one round trip, and the schema is a typed contract the app can generate code from. Internal high-volume calls: gRPC, because Protobuf contracts are strict and compact, HTTP/2 multiplexes many calls over one connection, it supports streaming, and deadlines and cancellation are built in. The style is chosen per boundary, not per company, and the cost of each matters: GraphQL needs query cost limits and loses simple URL caching; gRPC needs HTTP/2 end to end and is awkward in browsers without a proxy such as gRPC-Web.
WHAT A STRONG ANSWER COVERS
- Picks per boundary and consumer, not one style everywhere.
- REST for broad reach, HTTP caching, and simple public contracts.
- GraphQL for client-shaped, nested reads; names its costs (query cost, caching, N+1).
- gRPC for internal, typed, low-latency calls with streaming and deadlines.
- Mentions browser limits of gRPC and the need for proxies or gRPC-Web.
RED FLAGS
- "GraphQL replaces REST" or "gRPC is always faster, so use it everywhere."
- Choosing GraphQL for a public API without mentioning cost limits or abuse.
- Ignoring who the consumer is and what tooling they have.
LIKELY FOLLOW-UPS
- How would a BFF change the mobile answer?
- What would make you expose gRPC to external partners?
How did your answer compare?
SOURCES
A client calls `POST /orders`, the connection drops before the response arrives, and the client retries. How do you design the API so the retry does not create a second order?
Reveal model answer
Have the client send an `Idempotency-Key` header (a UUID generated once per logical operation and reused on every retry). The server stores the key with a fingerprint of the request and the final response, inside the same transaction as the side effect or with a unique constraint, so a repeated key returns the stored response instead of creating another order. While the first request is still in flight a retry with the same key gets 409 Conflict, and reusing a key with a different body gets 422, as the IETF draft recommends. Keys expire after a window such as 24 hours. An alternative is to let the client choose the resource ID and use `PUT /orders/{id}`, which is naturally idempotent.
WHAT A STRONG ANSWER COVERS
- Client-generated key reused across retries of the same operation.
- Key, request fingerprint, and response stored atomically with the side effect.
- Concurrent duplicate (409) and key reuse with a different body (422) handled.
- Keys have a TTL; scope them per client or account.
- Client-chosen IDs with PUT as an alternative.
RED FLAGS
- "Check if an identical order exists in the last minute" as the de-duplication rule.
- Storing the key in a cache separate from the database write, so a crash leaves them inconsistent.
- Generating a new key on each retry.
LIKELY FOLLOW-UPS
- What do you return if the first attempt failed with a 500?
- How do downstream calls (payment provider) stay idempotent too?
How did your answer compare?
SOURCES
What is the difference between PUT, PATCH, and POST? A teammate writes `PATCH /users/42` with body `{ "loginCount": "+1" }`. What is wrong?
Reveal model answer
PUT replaces the target resource's state with the request body (or creates it at that URL) and is idempotent: sending it twice leaves the same state. PATCH applies a partial change described by a patch document, such as JSON Merge Patch or JSON Patch, and is not idempotent by definition, though a patch that sets fields to fixed values happens to be. POST asks the target to process the body, typically creating a subordinate resource and returning 201 with a Location header, and is neither safe nor idempotent. The teammate's PATCH is a relative increment, so a retry counts twice and two clients can race; use an explicit action endpoint with an idempotency key, or make the PATCH conditional with `If-Match` on an ETag so a stale write fails with 412.
WHAT A STRONG ANSWER COVERS
- PUT is full replacement and idempotent.
- PATCH is partial; idempotency depends on the patch document.
- POST is processing or creation; 201 with Location for creation.
- Relative updates are unsafe to retry; use conditional requests or idempotency keys.
RED FLAGS
- "PUT is for update, POST is for create" with no mention of idempotency.
- Treating PATCH as automatically idempotent.
- Using PUT with a partial body and silently nulling missing fields.
LIKELY FOLLOW-UPS
- When would you use JSON Patch rather than JSON Merge Patch?
- How do ETags prevent lost updates?
How did your answer compare?
SOURCES
An activity feed endpoint uses `GET /events?page=500&limit=50`, it gets slower on later pages, and users see duplicates when new events arrive. What would you change?
Reveal model answer
Switch from offset to cursor (keyset) pagination: the server returns an opaque `next_page_token` that encodes the last seen sort key, such as `(created_at, id)`, and the next query is `WHERE (created_at, id) < (:ts, :id) ORDER BY created_at DESC, id DESC LIMIT 50`. That query uses an index and costs the same on page 500 as on page 1, while `OFFSET 25000` makes the database read and discard every skipped row. Cursors are also stable when rows are inserted ahead of the reader, so items do not shift between pages. Make the token opaque so clients cannot depend on its contents, cap the page size, keep filters fixed across pages, and keep offset only where users genuinely need to jump to page N of a small, stable list.
WHAT A STRONG ANSWER COVERS
- Explains why OFFSET degrades (rows scanned and discarded).
- Keyset on a unique, indexed sort key with a tiebreaker.
- Stable results under concurrent inserts.
- Opaque tokens, capped page size, filters fixed across pages.
- Knows the trade-off: no random page jumps or cheap total counts.
RED FLAGS
- Sorting only by a non-unique timestamp, so ties are skipped or repeated.
- Exposing a raw ID as the cursor and then being unable to change the sort.
- "Just add an index" without addressing OFFSET.
LIKELY FOLLOW-UPS
- How do you paginate when the sort order is user-selectable?
- How would you show an approximate total count?
How did your answer compare?
SOURCES
You need to rename a field and change an enum in an API used by mobile apps that stay installed for years. How do you version and roll this out?
Reveal model answer
First try not to break anything: add the new field next to the old one, keep both populated, treat unknown enum values as a supported case on clients, and deprecate the old field with a `Deprecation` or `Sunset` signal and usage metrics. When a break is unavoidable, bump a major version, either in the path (`/v2/...`, as Google's AIP-185 requires) or as a dated version pinned per account and selected by a header, as Stripe does, with a translation layer that converts between versions at the edge so the core runs one model. Mobile makes this harder because old binaries never update, so you keep old versions until usage falls below a threshold, and you may add a minimum-supported-version check that forces an upgrade. Contract tests or schema checks in CI catch accidental breaking changes before they ship.
WHAT A STRONG ANSWER COVERS
- Prefers additive, backward-compatible changes and tolerant readers.
- Unknown enum values must not crash clients.
- A versioning scheme (path major version or dated header) and a deprecation policy.
- Translation at the edge so business logic does not fork per version.
- Usage metrics and forced-upgrade paths for long-lived mobile clients.
- Automated breaking-change detection in CI.
RED FLAGS
- Versioning every endpoint on every change.
- Removing the old field as soon as the new web client ships.
- No way to know who still calls the old version.
LIKELY FOLLOW-UPS
- How do you version GraphQL or gRPC APIs differently?
- What belongs in a deprecation policy?
How did your answer compare?
SOURCES
Compare short polling, long polling, Server-Sent Events, and WebSockets. Which would you use for a live sports score page, a collaborative editor, and a nightly report status?
Reveal model answer
Short polling sends a request on a timer: trivial and cache-friendly, but it wastes requests and adds up to one interval of latency. Long polling holds the request open until there is data or a timeout, then the client re-requests: near real time over plain HTTP, but every message costs a new request. SSE is a single long-lived HTTP response streaming `text/event-stream` from server to client, with built-in reconnection and `Last-Event-ID` resume, but it is one-way and, over HTTP/1.1, limited to six connections per browser and domain across all tabs (HTTP/2 multiplexes streams over one connection, typically up to 100). WebSockets give a full-duplex, low-overhead channel after an HTTP upgrade, but you build heartbeats, reconnection, resume, and auth refresh yourself, and they are stateful to load-balance. Live scores: SSE, since data only flows down and it passes through HTTP infrastructure. Collaborative editor: WebSockets, since both sides send frequent small messages. Nightly report status: polling every few seconds, or a single notification, because real-time machinery is not worth it.
WHAT A STRONG ANSWER COVERS
- Latency and overhead trade-offs of each transport.
- SSE is one-way, auto-reconnects, and resumes with Last-Event-ID.
- WebSockets are bidirectional but need app-level heartbeats, reconnect, and resume.
- HTTP/1.1 connection limit for SSE and why HTTP/2 removes it.
- Picks the simplest transport that meets the latency need.
RED FLAGS
- WebSockets for everything, including one-way feeds.
- Not knowing that SSE reconnects automatically.
- Ignoring proxies, load balancers, and idle timeouts.
LIKELY FOLLOW-UPS
- How would you authenticate an SSE or WebSocket connection?
- What changes behind a corporate proxy that kills idle connections?
How did your answer compare?
SOURCES
Your chat service works on one server. With three servers behind a load balancer, messages sometimes never reach users. Why, and how do you scale WebSockets across servers?
Reveal model answer
Each connection lives on exactly one server, so a message published on server A reaches only the sockets on A; users connected to B and C miss it. Add a fan-out layer: every server subscribes to a pub/sub backplane (Redis pub/sub or streams, NATS, Kafka) on the channels its connected users care about, and publishers write to the channel rather than to local sockets; for targeted delivery keep a registry of user to server. Sticky sessions do not fix fan-out, they only help with per-connection state, which is better kept in a shared store so any server can take over a reconnecting client. Around that: balance on connection count rather than requests, drain a server before removal by refusing new connections and asking clients to reconnect gradually, keep heartbeats shorter than proxy idle timeouts, and require clients to reconnect with exponential backoff and jitter to avoid reconnect storms after a deploy.
WHAT A STRONG ANSWER COVERS
- Identifies that connections are server-local, so fan-out must cross servers.
- Pub/sub backplane or a user-to-gateway registry for routing.
- Sticky sessions are not a fan-out solution; externalize session state.
- Connection-count balancing, graceful draining, and heartbeats.
- Jittered reconnects to prevent thundering herds.
RED FLAGS
- "Turn on sticky sessions" as the whole answer.
- Broadcasting every message to every server without channel filtering at large scale.
- No plan for what happens to connections during a deploy.
LIKELY FOLLOW-UPS
- How would you deliver to a channel with a million subscribers?
- What does the backplane cost you in ordering and delivery guarantees?
How did your answer compare?
SOURCES
A mobile client's WebSocket drops for 20 seconds in a tunnel. How do you make sure it does not lose messages that were sent while it was offline?
Reveal model answer
Give each message a monotonically increasing sequence number per stream (per conversation, order, or user) and persist messages before fanning them out. The client remembers the last sequence it processed and, on reconnect, sends it in the handshake (the same idea as SSE's `Last-Event-ID`); the server replays everything after it from a retained log, or tells the client to do a full resync if the gap is older than the retention window. Delivery is then at least once, so the client de-duplicates by message ID and applies messages in sequence order, buffering when it sees a gap. Detect dead connections with heartbeats rather than waiting for TCP, reconnect with backoff and jitter, and treat the socket as a fast path on top of a durable source of truth, with push notifications for clients that are fully offline.
WHAT A STRONG ANSWER COVERS
- Per-stream sequence numbers and persist-then-publish.
- Client sends its last seen position on reconnect; server replays the gap.
- Bounded retention with a fallback full resync.
- At-least-once delivery with client-side de-duplication and ordering.
- Heartbeats for dead-connection detection; jittered backoff.
RED FLAGS
- Assuming the socket is reliable because it runs over TCP.
- Pub/sub alone (fire-and-forget) with no replay log.
- Ordering by client or server wall-clock timestamps.
LIKELY FOLLOW-UPS
- How long should the replay log retain messages?
- How do you handle a client that is days behind?
How did your answer compare?
SOURCES
You are the provider sending webhooks to customers. How do you design signing, retries, and ordering, and what must receivers do?
Reveal model answer
Sign each delivery with HMAC-SHA256 (or an asymmetric signature) over the message ID, a timestamp, and the raw body, as Standard Webhooks does with `msg_id.timestamp.payload`; receivers verify against the raw bytes with a constant-time comparison and reject old timestamps to block replays, and you support several active secrets so customers can rotate. Deliver from a durable outbox and queue, treat only 2xx as success, and retry with exponential backoff and jitter over hours or days (Stripe retries live events for up to three days), with a dead-letter view and manual resend. Do not promise ordering: put a stable event ID and a resource version or timestamp in each event so receivers can de-duplicate and ignore stale updates, or send thin events that tell them to fetch the current state. Receivers should verify, store the event, return 2xx quickly, and process asynchronously and idempotently.
WHAT A STRONG ANSWER COVERS
- Signature over ID, timestamp, and raw body; replay window; secret rotation.
- Durable outbox, backoff with jitter, dead-letter and resend tooling.
- At-least-once delivery with a stable event ID for de-duplication.
- No ordering guarantee; versions or fetch-latest to handle out-of-order events.
- Receivers acknowledge fast and process from a queue.
RED FLAGS
- Verifying the signature against re-serialized JSON.
- Doing slow work before returning 200, causing timeouts and retries.
- Assuming events arrive once and in order.
LIKELY FOLLOW-UPS
- How would you protect your webhook sender from SSRF via customer URLs?
- When would you disable a customer's endpoint?
How did your answer compare?
SOURCES
A GraphQL query `{ orders(first: 100) { customer { name } items { product { title } } } }` makes hundreds of database calls, and someone sends a deeply nested query that takes down the server. How do you fix both?
Reveal model answer
The N+1 comes from resolvers loading `customer` and `product` one parent at a time; add a per-request DataLoader (or equivalent) that collects keys during a tick and issues one batched query such as `WHERE id IN (...)`, with a per-request cache so the same key is loaded once. The outage is a demand-control problem: GraphQL lets the client choose the shape, so limit depth (with a tighter limit on nested lists), breadth and aliases, require pagination with a capped `first` on list fields, and run cost analysis that assigns weights to fields and rejects operations over a budget before execution. For first-party clients, use trusted documents (persisted queries) so only reviewed operations run. Rate limit by computed cost rather than request count, and add timeouts on resolvers and data sources.
WHAT A STRONG ANSWER COVERS
- Explains why naive resolvers cause N+1 and how batching fixes it.
- Per-request DataLoader cache, not a global one (auth and staleness).
- Depth, breadth, alias, and pagination limits.
- Static cost analysis with a per-operation budget.
- Trusted documents for first-party clients; cost-based rate limiting.
RED FLAGS
- "Add a cache" without batching.
- Sharing a DataLoader across users or requests.
- Relying only on request-count rate limits for GraphQL.
LIKELY FOLLOW-UPS
- How would you estimate the cost of a connection field?
- Should introspection be on in production?
How did your answer compare?
SOURCES
After scaling a gRPC service from 3 to 10 pods, the new pods sit idle while the old ones are overloaded, and some calls hang forever. What is going on?
Reveal model answer
gRPC multiplexes every call over a long-lived HTTP/2 connection, so a layer-4 (TCP) load balancer such as a plain Kubernetes Service balances connections, not requests; existing clients keep their connections to the original three pods and new pods only get new clients. Fix it with request-level balancing: client-side load balancing over a headless Service or xDS with a `round_robin` policy, or an L7 proxy or service mesh (Envoy) that understands HTTP/2; a server-side maximum connection age also forces clients to reconnect periodically and spread out. The hangs are because gRPC sets no deadline by default, so a client waits indefinitely; every call needs a deadline, servers should stop work when it passes (the call is cancelled), and the remaining deadline should propagate to downstream calls, which gRPC sends as a relative timeout to avoid clock-skew problems.
WHAT A STRONG ANSWER COVERS
- HTTP/2 long-lived connections defeat connection-level (L4) balancing.
- Client-side LB, lookaside/xDS, or an L7 proxy/mesh as fixes.
- Max connection age to rebalance over time.
- No default deadline in gRPC; always set one.
- Deadline propagation and server-side cancellation handling.
RED FLAGS
- Scaling up more pods without understanding why load is uneven.
- Relying on a TCP load balancer for gRPC traffic.
- Setting deadlines only at the edge and not propagating them.
LIKELY FOLLOW-UPS
- How should a service react to DEADLINE_EXCEEDED from a dependency?
- What is the cost of running a sidecar proxy per pod?
How did your answer compare?
SOURCES
The browser times out after 10 s, the API gateway after 30 s, the service after 60 s, and each layer retries 3 times. What is wrong, and how would you set timeouts and retries?
Reveal model answer
The timeouts are inverted: the caller gives up at 10 s while the layers below keep working, and their retries keep load on a struggling dependency long after anyone is waiting. Retries also multiply across layers: three attempts at each of several layers can turn one user request into dozens of backend calls; the AWS Builders' Library example is 3 retries at 5 layers giving 243 times the load. Set timeouts from the outside in so each inner layer's budget is smaller than its caller's, or better, propagate a deadline so every hop knows the remaining time. Base each timeout on the dependency's high-percentile latency (such as p99.9) rather than a round number. Retry at one layer only, usually closest to the failure, only for idempotent operations and retryable errors, with capped exponential backoff and jitter and a retry budget or token bucket, and add circuit breaking or load shedding so overload fails fast.
WHAT A STRONG ANSWER COVERS
- Inner timeouts must be shorter than outer ones, or use deadline propagation.
- Retry amplification across layers and its multiplicative effect.
- Timeouts chosen from measured latency percentiles.
- Retry in one place, only idempotent and retryable errors.
- Backoff with jitter, retry budgets, and fail-fast mechanisms.
RED FLAGS
- Raising every timeout when things get slow.
- Retrying non-idempotent POSTs without idempotency keys.
- Fixed-interval retries without jitter.
LIKELY FOLLOW-UPS
- How does a retry budget differ from a circuit breaker?
- How do you test timeout behavior before production?
How did your answer compare?
SOURCES
Checkout calls inventory, payment, email, loyalty points, and analytics synchronously, and any of them being slow breaks checkout. Which calls should stay synchronous and which should become asynchronous messages?
Reveal model answer
Keep a call synchronous only when the user's response depends on its result right now: reserving inventory and authorizing payment decide whether the order succeeds, so they stay request-response with deadlines and idempotency keys. Email, loyalty points, and analytics are reactions to the fact that an order was placed, so publish an `OrderPlaced` event and let those consumers process it independently; they gain temporal decoupling, load leveling, and retries, and checkout no longer fails when they are down. Publish reliably with a transactional outbox so the order row and the event cannot diverge, and make consumers idempotent because delivery is at least once. The cost of async is eventual consistency, harder debugging (needs correlation IDs and tracing), and the need for dead-letter handling and monitoring of queue lag.
WHAT A STRONG ANSWER COVERS
- Sync when the caller needs the answer to proceed; async for side effects.
- Commands versus events, and temporal decoupling.
- Transactional outbox to avoid dual writes.
- Idempotent consumers, DLQs, and lag monitoring.
- Names the costs: eventual consistency and observability.
RED FLAGS
- "Make everything async" including the payment decision the user waits on.
- Publishing to the broker and writing the database separately with no outbox.
- Assuming exactly-once delivery.
LIKELY FOLLOW-UPS
- How would you tell the user later that payment failed if it were async?
- When does a saga make sense here?
How did your answer compare?
SOURCES
Design real-time order tracking for a food delivery app: customers watch order status and the courier's location on a map.
Reveal model answer
Requirements: status changes (accepted, preparing, picked up, delivered) must be reliable and ordered; courier location updates every few seconds can be lossy, since only the latest matters; tens of thousands of active orders, each with a handful of viewers (customer, support, restaurant); mobile clients on flaky networks. Transport: couriers send location over HTTPS batches or a WebSocket; customers receive a one-way stream, so SSE (or a WebSocket if the app already has one) per order, with push notifications for key status changes when the app is backgrounded. Fan-out: the order service persists status changes with a per-order sequence number and publishes them, plus location updates (throttled and coalesced to the latest point), to a per-order channel on a pub/sub layer; stateless edge servers subscribe to channels for the orders their connected clients watch. Reconnect and resume: the client reconnects with backoff and jitter and sends its last sequence number; the server replays missed status events and sends the current location snapshot rather than the location history. Scale: balance edge servers on connection count, store latest location in an in-memory store with a TTL, write location history asynchronously for analytics, drain connections gradually on deploy, and degrade to polling the order status endpoint if streaming fails.
WHAT A STRONG ANSWER COVERS
- Separates reliable, ordered status events from lossy, latest-wins location updates.
- Justifies the transport (SSE or WebSocket down, HTTPS or WebSocket up, push when backgrounded).
- Per-order channels on a pub/sub layer with stateless edge servers.
- Resume with sequence numbers; snapshot for location instead of replay.
- Capacity and failure handling: connection balancing, throttling, draining, polling fallback.
RED FLAGS
- Writing every location ping synchronously to the primary database.
- Treating location and status with the same delivery guarantees.
- No plan for backgrounded apps or reconnects.
LIKELY FOLLOW-UPS
- How would you show an ETA that does not jump around?
- How do you authorize who may watch a given order?
How did your answer compare?
SOURCES