Scenario Debugging

Quick Scenario Round: 15 Questions, 15 Clear Answers

CORS failing only in production, random logouts, 429 errors caused by your own frontend, growing server memory and more. Each answer: find it, fix it, stop it coming back.

Lesson 0925 min read#scenario#interview#cheat-sheet

On this page

  1. Words used in this lesson
  2. How to use this lesson
  3. The scenarios
  4. Interview questions (practise out loud)
  5. Key takeaways
  6. What's next

In one line: every scenario answer has the same three steps. Diagnose (what evidence do you collect first?). Fix (the smallest change that removes the cause). Prevent (the guard that stops it coming back). Practise saying them out loud.

This is the revision lesson. It has fifteen realistic scenarios from the whole series. Each one is answered the way an interviewer wants to hear it. Try to answer before you read each one.

Words used in this lesson

Short definitions, so every answer below is easy to follow. Skim them first and come back when a word is new.

Web and network

  • Origin is the trio scheme + host + port of a page, for example https://app.example.com:443. Two URLs with any difference in the trio are different origins.
  • CORS (Cross-Origin Resource Sharing) is a browser rule that blocks a page from reading a response from a different origin, unless that server allows it with Access-Control-Allow-* headers. The browser enforces it, not the server.
  • Allow-list is a list of the only values that are permitted, for example the origins that may call the API.
  • Cookie is a small piece of data that the server asks the browser to store and send back on later requests. Its attributes are Max-Age (how long it lives), Domain and Path (where it is sent), Secure (HTTPS only) and SameSite (whether it is sent on cross-site requests).
  • Load balancer is a server that spreads incoming requests across several copies of your app. Proxy is a server that sits between the client and your app and passes requests on. Gateway is a proxy at the front door of an API.
  • TLS is the protocol that encrypts HTTPS traffic. "TLS ends at the proxy" means the proxy decrypts the traffic and talks plain HTTP to your app.
  • CDN (content delivery network) is a set of servers around the world that keep copies of your files close to users.
  • HTTP status codes: 401 means "not logged in", 429 means "too many requests", 502 means "the gateway got a bad answer from the app", 504 means "the gateway waited too long for the app".
  • TTL (time to live) is how long a stored item (a cache entry, a session, a DNS answer) is kept before it expires.

Sessions and tokens

  • Session is the server's memory of a logged-in user. Session store is where the sessions live (for example Redis, a fast in-memory database).
  • Access token is a short-lived proof of login that the client sends with each request. Refresh token is a longer-lived proof that is used only to get a new access token.

React and the browser

  • Strict Mode is a React development helper that runs some code twice to expose bugs. It does not run twice in production.
  • Stack trace is the list of function calls that led to an error.
  • Source map is a file that maps minified production code back to your original code, so error reports are readable.
  • Polyfill is code that adds a missing browser feature. Transpile means to convert new JavaScript into older JavaScript.
  • CSP (Content Security Policy) is a header that tells the browser which scripts and resources the page may load.
  • LCP (Largest Contentful Paint) is the time until the biggest visible element (usually a hero image or a heading) is drawn. TTFB (Time to First Byte) is the time until the first byte of the response arrives.
  • srcset is an image attribute that offers several file sizes so the browser picks one that fits the screen.

Backend and data

  • N+1 query problem is a pattern where code runs 1 query for a list and then N more queries, one for each item. Batching fixes it.
  • DataLoader is a small library that collects many single lookups made in the same moment and runs them as one batch query.
  • EXPLAIN is a database command that shows the plan the database will use to run a query. EXPLAIN ANALYZE also runs it and shows real times.
  • Connection pool is a set of open database connections that the app reuses, because opening a new one is slow.
  • LRU (least recently used) is a rule for a size-limited cache: when it is full, remove the item that was used longest ago.
  • Idempotent means that doing the same request twice has the same result as doing it once. An idempotency key is a unique ID that the client sends with a request, so the server can recognise a repeat and not do it again.
  • Index is an extra data structure that lets the database find rows without reading the whole table. A B-tree index is the usual kind. It is good for equality and ranges, and (with the right operator class or collation) for text that starts with a known prefix. A GIN trigram index splits text into three-letter pieces so that searches like %term% can use an index.
  • Heap snapshot is a saved picture of everything in memory at one moment, so you can compare two pictures and see what grew.

How to use this lesson

  1. Read the symptom. Say your first three diagnostic steps out loud.
  2. Read the answer and compare. Notice the evidence that each step collects.
  3. Read the Prevent line again. Senior engineers always end with it.

The scenarios

1. "It works locally but fails in production with a CORS error."

Diagnose: open the failing request in the Network tab. Look at the OPTIONS preflight (a check request the browser sends first; look at its status and response headers). Then look at the real request. Compare the page's origin with the server's allow-list: scheme, host, port and www. Compare the preflight's Access-Control-Request-Headers and Access-Control-Request-Method with the server's Access-Control-Allow-Headers and Access-Control-Allow-Methods. A custom header such as Authorization that is missing from the list fails the preflight. Check who sent the error response: the app or a proxy/gateway. A 502 or 403 without CORS headers shows up as "a CORS error". Also look at credentials and Access-Control-Allow-Credentials. Check if Allow-Origin is * while cookies are in use. (This combination is not allowed.)

Fix: add the exact production origin to the allow-list. Register the CORS middleware (code that runs on every request before your route handlers) before auth, so preflights get a 2xx answer. The preflight does not carry cookies, so an auth check would reject it. Send Vary: Origin when you echo origins back. If a real 5xx error hides behind the CORS message, fix that too. Better still: make the API same-origin through a proxy or a BFF (a backend that serves your frontend).

Prevent: keep an allow-list for each environment in config. Add an integration test for the preflight. Use a staging environment with the same kind of domain as production. (Browser Contract, Lesson 04)

2. "Users get logged out at random."

Diagnose: is it random for each user, or does it depend on time? Check the cookie attributes (Max-Age, Domain, SameSite, Secure). Check that the cookie is really being set on the response. Then look at these possible causes:

  • The session store: Redis evicting keys, a TTL shorter than you expect, or many app instances with no shared store.
  • Token expiry and refresh: the access token expires and the refresh fails. Or two tabs refresh at the same time, and rotation (a new refresh token each time) cancels one of them.
  • Clock skew (two machines disagree about the time).
  • A deploy that changed the signing key or SESSION_SECRET.
  • A load balancer that sends requests to instances with different secrets.
  • Browser privacy features (Safari ITP) that shorten cookies set by scripts.

Fix: use a shared session store with the right TTL, and the same secret on every instance. Run refresh calls one at a time (keep a single refresh promise in flight). Keep a short grace window for old refresh tokens, so races do not break sessions. Set cookies from the server with the right attributes.

Prevent: log session creation, refresh and invalidation with a request ID. Set an alert for sudden spikes in 401 errors. Test refresh with several tabs. (Browser Contract, Lessons 02 and 03)

3. "The API returns 429 during normal use."

Diagnose: which key does the limiter use (IP, user, API key)? Check whether the frontend creates the traffic: duplicate fetches in effects, no request dedupe, polling in every tab, retry loops with no backoff (waiting longer between tries), or Strict Mode in development. Or many users may share one NAT IP (one public address for a whole office) while the limit is per IP.

Fix: dedupe requests with a shared cache (React Query/SWR). Poll less, or use SSE (the server pushes updates to the browser). Pause when the tab is hidden. Back off using Retry-After. Limit per user or API key, not per IP. Raise the limits for shared IPs that you know are good.

Prevent: send RateLimit-* headers. Put requests per user per endpoint on a dashboard. Add a client-side request budget to your tests. (Lesson 05, Practical Design Problems, Lesson 02)

4. "Server memory grows over several days."

Diagnose: plot heap against RSS (heap is the memory for your objects; RSS is all the memory the process uses). Take heap snapshots: one at the start and one hours later (run Node with node --inspect and use the Memory tab in Chrome DevTools, or call v8.writeHeapSnapshot()). Compare them. If the heap is flat but RSS grows, the leak is outside the heap (for example Buffers or native code). Look at the retainers (the objects that still point to the growing objects, which keeps them from being cleaned up). Common causes:

  • Caches and maps in memory with no size limit.
  • Listeners added on each request (MaxListenersExceededWarning).
  • Global arrays that collect data.
  • Streams or connections that are never closed.
  • Timers that are never cleared.
  • Closures that hold large request objects. (A closure is a function that remembers the variables around it.)

Fix: put a limit on caches (LRU, which drops the least recently used items, with a size or TTL). Remove listeners and clear timers when the work ends. Close streams. Do not collect data on each request. As a short-term step, restart instances on a schedule or at a memory limit while you look for the retainer.

Prevent: add memory and heap metrics with alerts. Run a soak test in staging (a long test under steady load, to see slow leaks). In code review, look for collections at module level. (Lesson 04, Backend, Lesson 10)

5. "After a deploy, some users get a blank page or ChunkLoadError."

Diagnose: the HTML (or an old Service Worker) points to old chunk names that the new deploy deleted. Or a CDN cached old HTML. (A chunk is one piece of your split JavaScript. A Service Worker is a script that the browser runs in the background. It sits between the page and the network and can serve saved files.) Check Cache-Control on the HTML. Check if the old files still exist. Check the CDN cache status headers and the Service Worker version.

Fix: serve HTML with no-cache (the browser may keep a copy, but must check with the server before using it). Put a content hash in each file name (for example app.3f9a1c.js), so a changed file gets a new name. Keep the previous build's files available for a while. Purge the CDN for the paths that changed. Catch chunk-load errors and reload the page once. If you use a Service Worker, make sure it updates itself and does not serve old HTML.

Prevent: use immutable hashed files with short-lived HTML. Keep the last N builds when you deploy. Set a Sentry alert for ChunkLoadError. (Browser Contract, Lesson 05)

6. "Cookies are missing after a deploy or behind the load balancer."

Diagnose: look at the Set-Cookie header and the Application tab. Common causes:

  • The app thinks the request is HTTP, because TLS ends at the proxy. So a Secure cookie is never set. Fix trust proxy (an Express setting that tells the app to believe the proxy) and X-Forwarded-Proto (the header in which the proxy says whether the original request used HTTPS).
  • Domain or Path does not match.
  • SameSite blocks a cross-site flow.
  • The cookie name changed, or the rules for the __Host- prefix are not met (that name prefix requires Secure, Path=/ and no Domain).
  • The cookie is too large (over about 4 KB) and the browser rejects it.
  • A CDN removes or caches Set-Cookie.

Fix: set the proxy trust setting. Align the attributes. Keep cookies small. Make sure CDN rules do not cache or remove responses for logged-in users.

Prevent: add an end-to-end test that logs in on a stack like production, with TLS ending at the proxy. (Browser Contract, Lesson 01)

7. "A GraphQL query is slow."

Diagnose: use tracing to see the time of each resolver and the number of database queries. Look for these problems:

  • N+1: the same query runs once for each parent item (see the word list).
  • Missing indexes (check with EXPLAIN).
  • Over-fetching: deep or large lists with no pagination.
  • Expensive fields that every query asks for.

Fix: batch queries with DataLoader (one per request). Add indexes. Force pagination and set a limit on query depth or complexity. Move heavy fields to separate queries or to a cache. Think about persisted queries (saved queries that the client calls by ID) so they can be cached.

Prevent: add query-count checks to your tests. Trace resolvers in production. Set cost limits on the API. (Backend, Lessons 08 and 09)

8. "Maximum update depth exceeded" / an infinite render loop.

Diagnose: find the component in the stack trace. Look for setState called during render. Or look for an effect (a useEffect function that runs after render) whose dependencies (the values in its dependency array) change every time. For example, an object or array made during render is used as a dependency. Or an effect sets state that is also in its own dependencies. A very common cause is onClick={setOpen(true)}: this calls the function during render. Write onClick={() => setOpen(true)} instead.

Fix: move the update into an event handler. Work out the value during render, and do not copy it into state. Make the dependencies stable: useMemo (a hook that remembers a value between renders) or useCallback (a hook that remembers a function), or move constants outside the component. If an effect must set state, add a condition so it stops when the value is already correct.

Prevent: use the react-hooks/exhaustive-deps lint rule (a code check that warns when an effect's dependency list is incomplete). In code review, watch for "state worked out from props". Write tests. (Frontend Depth, Lesson 01)

9. "Hydration failed because the server rendered HTML didn't match the client" (Next.js).

Diagnose: the first render in the browser gave different output from the server. (Hydration is when React takes the server's HTML and makes it interactive. React expects both outputs to match.) Typical causes:

  • Using Date.now(), Math.random(), window, localStorage or navigator during render.
  • Formatting by language or timezone that differs between the server and the browser.
  • Invalid HTML nesting (a <div> inside a <p>).
  • Browser extensions that add nodes.
  • Conditions that depend on state that exists only in the browser.

Fix: render the same thing on both sides. Then update after mount in an effect for values that exist only in the browser. For harmless text such as timestamps, you can use suppressHydrationWarning. Fix invalid nesting. Format dates with a fixed locale and timezone, or only in the browser. suppressHydrationWarning is a prop that tells React not to warn about a text difference on that one element. It works one level deep only, and it does not repair other differences.

Prevent: lint for impure render code. Test hydration in CI. Do not use browser-only branches in the first render. (Frontend Depth, Lesson 02)

10. "Intermittent 502 or 504 errors from the load balancer, but the app logs look fine."

Diagnose: check the timeouts at every hop. A classic cause is this: the load balancer's idle timeout (for example 60 s on an AWS ALB) is longer than the Node server's keep-alive timeout (the default is 5 s). The server closes an idle connection at the moment the load balancer reuses it. This gives random 502 errors. Also check:

  • Request timeouts on slow endpoints (504).
  • Health checks that fail during deploys.
  • Instances that are killed without a graceful shutdown (finishing current requests before stopping).

Fix: set server.keepAliveTimeout higher than the load balancer's idle timeout (for example 65 s against 60 s). Keep server.headersTimeout higher than keepAliveTimeout. Add a graceful shutdown and proper health checks. Make the timeouts match across the proxy, load balancer and app. Move long work to async jobs.

Prevent: write down the timeout chain and test it. Alert on 5xx errors by source (load balancer or app). Run a deploy test with live traffic. (Backend, Lesson 04)

11. "After scaling out, the database says 'too many connections'."

Diagnose: the number of connections is instances × pool size (plus serverless concurrency). Check the pool settings. Check if each request, or each hot reload, makes a new client. Look at pg_stat_activity (a PostgreSQL view that lists every open connection and what it is doing) for idle connections.

Fix: use a pooler (a middle service that shares a few real database connections among many clients, such as PgBouncer or RDS Proxy). Use a smaller pool on each instance. Create a single database client per process, not one per request. Keep transactions short. Fix slow queries that hold connections.

Prevent: write the capacity maths in the runbook (your team's step-by-step guide). Alert on connection usage. Run load tests at the size you plan to reach. (Backend, Lesson 08)

12. "A customer was charged twice."

Diagnose: find both requests by idempotency key, request ID or order ID. (Idempotent means doing the same request twice has the same result as doing it once.) Were they two separate submissions from the client (a double-click, two tabs)? Or a client retry after a timeout? Or a redelivery from the server or queue? Check whether the payment provider got two identical charge requests.

Fix: refund the duplicate. Add idempotency (see the word list) from end to end: one client key for each purchase (the same key on every retry of that purchase, not a new key for each attempt), a server store for keys with a unique constraint, and the key passed on to the payment provider. Also add a natural unique constraint (one payment per order).

Prevent: make queue consumers idempotent. Run reconciliation reports (compare your records with the provider's). Set alerts for duplicate charges. (Lesson 05, Backend, Lesson 02)

13. "The page works for me, but some users see a blank screen."

Diagnose: get evidence from real users. Use error monitoring (Sentry), RUM, the mix of browsers and OS versions among the affected users, and the console error that fails. Typical causes:

  • A JS error with no error boundary. (An error boundary catches errors in part of the page. Without one, React removes the whole tree.)
  • A browser feature that is not supported (no transpile or polyfill).
  • A third-party script that fails.
  • Ad blockers or extensions that block an endpoint or script.
  • CSP (Content Security Policy) blocking something.
  • A chunk that failed to load.
  • Bad data from the API that crashes rendering (null where you expected an object).
  • A regional CDN failure.

Fix: reproduce the problem with the affected browser or data. Add error boundaries with fallback UIs (in Next.js, an error.tsx file for each route). An error boundary does not catch errors in event handlers or in async code, so handle those yourself. Guard against missing data. Fix the incompatibility. Make non-critical third parties fail quietly.

Prevent: put error boundaries at route level. Report errors with source maps. Keep a table of supported browsers and run cross-browser E2E (end-to-end: the test drives a real browser through the whole flow) tests on the critical flows. (Frontend Depth)

14. "The product page's LCP is slow on mobile."

Diagnose: look at real-user LCP by device. Find the LCP element and its four sub-parts (TTFB, resource load delay, resource load time and element render delay). Test on a slowed-down mobile profile. Typical causes:

  • The hero image is too large and has no srcset.
  • The image is found late (a CSS background, or added by JS).
  • The image is lazy-loaded.
  • Render-blocking CSS and a big JS bundle on slow CPUs.
  • High TTFB from an SSR (server-side rendering: the server builds the HTML) page that is not cached.

Fix: use responsive, modern images. Add fetchpriority="high" (an attribute that tells the browser to fetch this image first) and preload (an early fetch hint) where needed. Do not lazy-load the hero. Cut the blocking CSS and JS. Render on the server. Cache HTML at the edge (on servers close to users).

Prevent: set a performance budget. Run Lighthouse CI (an automatic page audit on every code change) with mobile settings. Set RUM alerts (RUM is real user monitoring: data from real visits) on p75 LCP (the value that 75 of 100 visits beat). (Lesson 06, Browser Contract, Lesson 09)

15. "Search is slow on a table with a million rows."

Diagnose: run EXPLAIN (ANALYZE, BUFFERS) on the query. Likely causes:

  • WHERE name LIKE '%term%' forces a sequential scan (reading every row). A leading wildcard cannot use a B-tree index.
  • lower(column) with no expression index.
  • Sorting on a column with no index, together with a big OFFSET.

Fix: use the right tool for the job:

  • A trigram (GIN) index for searching inside text.
  • PostgreSQL full-text search for searching words.
  • A dedicated search engine (OpenSearch/Elasticsearch/Typesense) when you need relevance ranking and scale.

Also add an expression index for case-insensitive equality. Switch to keyset pagination (the next page starts after the last row you saw, so there is no big OFFSET). On the client, debounce (wait for a pause in typing) and abort old requests.

Prevent: test with production-sized data. Monitor slow queries with pg_stat_statements (a PostgreSQL extension that records how often and how long each query runs). Check query plans (the output of EXPLAIN) in code review. (Backend, Lesson 07)

Interview questions (practise out loud)

"It works locally but fails in production with a CORS error." Walk me through it.

I check the preflight and the real request in the Network tab. I compare the page origin with the server's allow-list (scheme, host, port). I look for conflicts between credentials and the wildcard. I also check if the answer came from the app, or from a gateway that returned an error without CORS headers. To fix it, I add the exact origin to the allow-list, put CORS before auth, and fix any real 5xx errors. To prevent it, I use config for each environment and a preflight integration test.

"Users get logged out at random." What do you check?

I check the cookie attributes and whether the cookie is really set. I check the session store TTL and whether instances share it. I check that the secrets stay the same after deploys. I look for refresh-token races between tabs, and for expiry handling. I also look at clock skew and browser privacy limits. I add logging around session creation, refresh and invalidation, and I alert on spikes of 401 errors.

"The API returns 429 during normal use." Whose fault is it?

It may be the client's fault: duplicate fetches, polling in every tab, retries with no backoff, or Strict Mode in development. It may also be the limiter's key, for example many users behind one IP. On the client I dedupe requests and back off. On the server I limit by user or API key. I also send RateLimit headers, so clients can adjust.

"Server memory grows over several days." What next?

I compare heap snapshots over time to find the growing objects and what keeps them alive. I look for caches with no limit, listeners added on each request, global collections and resources that are never closed. I put limits on them or clean them up. I restart instances as a short-term step. Then I add heap metrics, alerts and a soak test.

What are the three steps of a good scenario answer?

First, diagnose: name the evidence and the tools. Second, fix: remove the root cause with the smallest effective change, and say the trade-offs. Third, prevent: stop it coming back with a test, a budget, an alert or a change in process.

Key takeaways

  • Always answer as Diagnose, then Fix, then Prevent. Name the tool and the evidence at each step.
  • Many production mysteries live between layers: proxies and TLS, timeouts that do not match, caches with old HTML, sessions across instances, pools against instance counts.
  • Tell client-caused load (duplicates, polling, retries) apart from real server limits.
  • End every answer with a guard: a test, a budget, an alert or a runbook.

What's next

That completes the Scenario Debugging series. Next, use these skills on design questions in Practical Design Problems. You can also go deeper on the frontend side in Senior Frontend Depth.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series