Scenario Debugging

Backend and API Scenario Bank

Sixteen server-side cases in find, fix, prevent form: duplicate webhooks, full connection pools, cron jobs that run twice, breaking API changes, stale caches, locking migrations and more.

Lesson 1122 min read#backend#scenario#api#interview

On this page

  1. The scenarios
  2. Practice drill
  3. Interview questions
  4. Key takeaways
  5. What's next

In one line: sixteen "what would you do?" questions about servers. Each answer has the same three parts: Diagnose (the evidence you collect first), Fix (the smallest change that removes the cause) and Prevent (the guard that stops the problem coming back).

Use this as a practice set. Cover the answer, say yours out loud, then compare. If a scenario has a full lesson behind it, the link is in brackets.

The scenarios

1. "An endpoint returns 500, but only for some users."

Diagnose: find a failing request by its request ID (a unique number for each request) in the logs. Read the error and the stack trace (the list of function calls that led to the error). Then ask what those users have in common. It could be a tenant (a customer account), a plan, or a region. It could be a data shape, such as a null where the code expects a value, a very long string, an unusual character, or a huge list. It could also be an old record created before a migration. Try to repeat the problem with that user's data in staging (a test copy of the system).

Fix: handle the missing or odd value. Add validation where data enters the system. Repair or backfill (fill in) the bad data.

Prevent: add a regression test (a test that stops an old bug returning) with the bad data shape. Add stricter database rules. NOT NULL forbids an empty value, and CHECK forbids values that break a rule you write. Use error tracking that groups errors by type, so you notice a new type at once. (Backend, Lessons 03 and 04)

2. "A coupon was redeemed twice (or stock went negative) under load."

Diagnose: this is a check-then-write race. A race happens when two requests run at the same time and get in each other's way. Both requests read "unused", and then both write "used". To repeat it, send many requests at the endpoint in parallel.

Fix: make the check and the write one atomic step (one step that cannot be split, like paying and getting the receipt in one move). There are three ways:

  • Run UPDATE coupons SET used_at = now() WHERE id = $1 AND used_at IS NULL and check how many rows changed. For stock, use the same idea: UPDATE items SET stock = stock - 1 WHERE id = $1 AND stock > 0.
  • Add a unique constraint (a database rule that refuses a second row with the same values) on (coupon_id, user_id).
  • Lock the row with SELECT … FOR UPDATE. This makes other requests wait until your transaction ends.

Prevent: write a concurrency test. Call the endpoint N times in parallel and check that exactly one call wins. Keep database constraints as the last line of defence. (Backend, Lesson 07)

3. "After a traffic increase we get 'timeout acquiring a connection' errors, but the database CPU is low."

Diagnose: the connection pool is used up. The database itself is fine. A connection pool is a fixed set of database connections that requests share, like a few taxis for many people. When all taxis are busy, new requests wait in a queue. Check the pool metrics. Look for long transactions (a transaction is a group of database steps that succeed or fail together; for example, an API call inside a transaction keeps it open too long). Look for slow queries that hold connections. Check if instances × pool size is bigger than the database limit.

Fix: make transactions shorter (no network calls inside them). Fix the slow queries. Choose pool sizes on purpose. Add a pooler (a separate tool that shares a few real database connections between many clients, such as PgBouncer or RDS Proxy). Add timeouts.

Prevent: set alerts on pool wait time and on connections in use. Run load tests. Put a connection budget in the capacity plan. (Backend, Lesson 08)

4. "The Node process is killed with an out-of-memory error every few hours."

Diagnose: is memory slowly growing (a leak, memory that is never given back) or does it jump suddenly (for example a huge response held in memory)? Look at the heap trend. The heap is the memory your program uses for its data. Take heap snapshots some time apart and compare them. Look for caches with no size limit, event listeners added on every request, big JSON built in memory, or readFile (which loads the whole file into memory) on large files. Also compare the container memory limit with Node's heap limit (--max-old-space-size). These are two different limits, and the container can kill the process before Node reports an error.

Fix: put a size limit on the cache. Stream large data (send it in small pieces). Remove listeners that are added on every request. Raise the limit only as a short-term help.

Prevent: add memory and event-loop metrics with alerts. The event loop is the part of Node that runs your code one task at a time. If it is blocked, everything waits. Run a soak test (a long test under steady load). Review module-level collections in code review. (Lesson 04, Backend, Lesson 10)

5. "A webhook from the payment provider was processed twice, and sometimes events arrive out of order."

Diagnose: a webhook is a message that the provider sends to your server. Providers send each message at least once, so you may get duplicates. They also do not promise the order. Check if you remove duplicates by event ID. Check if your handlers expect an order, for example a "refund" arriving before the "charge". Also check that your server answers a 2xx status (success code) fast. If it answers slowly, the provider sends the message again.

Fix:

  • Save processed event IDs with a unique constraint and ignore repeats.
  • Make handlers idempotent. This means running them twice gives the same result as running them once (like pressing a lift button twice).
  • Use the event's timestamp, or a state machine (a set of allowed steps) that rejects impossible changes.
  • When unsure, ask the provider's API for the current state instead of trusting the payload.
  • Check the signature (a code made with a shared secret, which proves the message really came from the provider). Save the raw event (or put it on a queue) first, then reply with 2xx quickly and do the work later in the background. If you reply before the event is saved, a crash loses it.

Prevent: test with duplicated and shuffled events. Add a reconciliation job that compares your data with the provider's. (Practical Design Problems, Lesson 07)

6. "The list endpoint gets slower the further users scroll."

Diagnose: this is offset pagination. OFFSET 100000 makes the database read 100,000 rows and throw them away. Confirm with EXPLAIN ANALYZE (a command that runs the query and shows how the database ran it) and the time for each page.

Fix: switch to keyset (cursor) pagination. It asks for "rows after this last item" instead of "skip N rows". Use a composite index (one index on several columns, for example (created_at, id)) and a unique tiebreaker (a second column such as id, so rows with the same date keep a fixed order). Cap limit and the maximum offset for old clients.

Prevent: test with data as big as production. Write down how the cursor works. (Practical Design Problems, Lesson 03)

7. "Our scheduled job ran twice, so customers received two emails."

Diagnose: two instances each ran the cron job (a job that runs on a timer), or an instance restarted during the run. The job is also not idempotent.

Fix: use a distributed lock or a leader, so only one instance runs the job. Options are Redis SET key value NX PX ttl (set the key only if it does not exist, and let it expire), a database advisory lock (a lock that your own code names and takes, not tied to a table row), or a managed scheduler that starts one run. Also make the job idempotent, for example with a "sent" flag or a unique (user, campaign) row. A distributed lock is a lock that all instances share, so only one can hold it at a time.

Prevent: run schedulers outside the web servers. Log the start and end of each job with a run ID. Set alerts for duplicate or missed runs.

8. "The monthly report endpoint times out and slows everything else."

Diagnose: heavy or slow work on the request path blocks workers or the event loop. The gateway (the server in front of your app that forwards requests) then gives up first, with a timeout error. (Backend, Lesson 10)

Fix: make it asynchronous (the request does not wait for the result). Return 202 with a job ID. Do the work in a worker. Store the result, for example in object storage. Let the client ask again later (this is polling) or send a notification. Also do the sums in SQL, stream the output (send it in small pieces), and cache reports that are the same.

Prevent: set a rule that nothing slower than about one second runs inline. Use separate worker pools. Limit how many jobs each user can start.

9. "A mobile app release broke because we renamed an API field."

Diagnose: a breaking change (a change that stops old clients working) went out without a version. Mobile users cannot all update at the same time.

Fix: bring back the old field next to the new one. This is an additive change (you only add things). Then move clients over slowly. Add the Deprecation and Sunset HTTP headers (extra information sent with a response) to say the old field will go away.

Prevent: add an OpenAPI (a file format that describes an API) breaking-change check in CI (the automatic build and test run). Use contract tests (tests that check that client and server still agree on the shape of the data). Have a versioning policy: additive by default, /v2 for breaking changes. Track usage for each client version. (Backend, Lessons 02 and 11)

10. "After a user updates their profile, the page still shows the old data for a minute."

Diagnose: a cache (a saved copy) is giving old data. It may be a CDN (a network of servers around the world that keep copies of your pages), Redis (a fast in-memory data store used as a cache) or the browser. Or a read replica is lagging. A replica is a copy of the main database, and lag means it is a little behind the primary. Check Cache-Control (the HTTP header that says how long a copy may be kept), the cache keys and the replica lag metric.

Fix: clear or update the cache when data is written. Use Cache-Control: private (only the user's own browser may keep it) or no-store (nobody may keep it) for personal data. Or read from the primary for a short time after a write. This is called "read your own writes".

Prevent: choose a clear way to clear each cache layer. Monitor replica lag. Write tests that write and then read. (Lesson 02)

11. "A database migration locked the table and the site froze."

Diagnose: a long ALTER TABLE or CREATE INDEX took a lock on a large table. The lock blocked reads and writes.

Fix: cancel the migration if it is safe. Then do it the online way:

  • Use CREATE INDEX CONCURRENTLY. It cannot run inside a transaction block, and if it fails it leaves an index marked invalid, so check for that and drop it.
  • Add new columns as nullable (they may be empty), then fill them in small batches.
  • Add constraints as NOT VALID (the rule applies to new rows only) and then check the old rows in a separate step with VALIDATE CONSTRAINT, which does not block writes.
  • Set a lock timeout (SET lock_timeout = '3s'), so the migration fails fast instead of making everything else wait behind it. A waiting ALTER TABLE blocks every query that comes after it.

Prevent: practise migrations on a copy that is as big as production. Review them for locking. Use expand and contract (first add the new thing, then move code over, then remove the old thing), so each step works with both old and new code. (Scenario 4 of Lesson 14)

12. "Some requests return 401 right after a deploy, then work again."

Diagnose: a rolling deploy replaces instances one by one, so old and new instances run together. Look for these causes. A JWT (JSON Web Token) is a signed token that proves who the user is. Its kid field names the key used to sign it.

  • Instances use different signing keys or secrets.
  • A JWKS cache (JWKS is a published list of public keys that servers use to check token signatures) has not learned about a new key.
  • Clock skew (clocks that differ a little) makes the exp (expires at) or nbf (not valid before) checks fail.
  • The keys of the session store (the place where login sessions are kept) were changed.

Fix: keep both old and new keys valid while you rotate (publish both in the JWKS). Refresh the key list when you see an unknown kid. Allow a small clock skew. Make secrets the same on all instances.

Prevent: write down the key-rotation steps. Test that tokens signed with the previous key still work. Use NTP (Network Time Protocol, which keeps clocks correct automatically) on all hosts. (Browser Contract, Lesson 02)

13. "Rate limits do not work: each instance allows the full limit."

Diagnose: the counters are kept in memory on each instance. With 10 instances, users get 10 times the limit.

Fix: count in a shared store such as Redis, with an atomic operation (one that cannot be split). Use a Lua script (a small script that Redis runs as one step), or INCR and EXPIRE sent together in one transaction. Do not send them as two separate calls, because a crash between them leaves a counter that never expires. Or limit at the gateway. Rate limiting means refusing requests when a client sends more than an allowed number in a time window.

Prevent: load-test the limits on the real deployed setup, not on one instance. (Practical Design Problems, Lesson 02)

14. "Users in different time zones see different 'today' in reports, and daily totals do not match."

Diagnose: the code mixes server time, UTC and local time. Or it cuts dates in the wrong zone. Daylight saving changes also create days with 23 or 25 hours.

Fix: store timestamps in UTC (the world reference time, without daylight saving) using the timestamptz type. Convert to the user's or the business's time zone only at the edge (when you show or report the data), with AT TIME ZONE. Define reports with an explicit zone and half-open ranges (>= start AND < end). A half-open range includes the start and leaves out the end, so no moment is counted twice.

Prevent: write tests around daylight saving and month ends. Use ISO 8601 UTC in the API (a standard text format such as 2026-05-01T10:00:00Z). Do not use toLocaleString on the server for data. (Backend, Lesson 02)

15. "A health check fails during deploys and instances are killed in a loop."

Diagnose: the liveness probe checks something slow or outside the app, such as the database. A short problem in that dependency then restarts healthy instances. Or the app needs more time to start than the probe allows. A probe is a regular check that the platform (for example Kubernetes) makes on your app. Here readiness or a startup probe is not set up.

Fix: keep two checks apart. Liveness asks "is the process alive?" and must be cheap. Readiness asks "is it ready for traffic?" and checks the dependencies. Set a fair start-up grace period. Handle SIGTERM (the signal the platform sends to ask your process to stop) with a graceful shutdown (finish current work, then stop).

Prevent: deploy with a canary (send a small part of traffic to the new version first). Test the probes. Set alerts on restart counts. (Backend, Lesson 04)

16. "We cannot trace a failing request across three services."

Diagnose: there is no shared correlation ID (one ID that follows a request everywhere). Logs are unstructured (plain sentences instead of fields such as requestId=… that tools can search). Each service logs in its own way.

Fix: create or accept X-Request-ID or traceparent at the edge. traceparent is the standard header for this, defined by W3C Trace Context. Pass it on every call to other services. Put it in every log line and in error responses. In Node, AsyncLocalStorage keeps a value such as the request ID available to all code that runs for one request. Use OpenTelemetry (a standard toolkit for collecting traces, which show the path of one request through many services) for traces.

Prevent: use one shared logging library. Add a test that checks the header is passed on. Build dashboards by trace ID. (Backend, Lesson 05)

Practice drill

Pick five scenarios at random. For each, give a 60-second answer with four parts: one piece of evidence you would look at, one guess about the root cause, one fix, one guard. Interviewers listen for this shape.

Interview questions

Scenario: after a traffic increase you see "timeout acquiring a connection" but the database CPU is low. What is happening?

The connection pool is used up, so requests wait in the application for a free connection. The causes are long transactions (often an outside call inside one), slow queries that hold connections, or too many instances for the database limit. I would look at the pool metrics and pg_stat_activity. Then I would shorten transactions, fix slow queries, size the pools and add a pooler. I would also set an alert on pool wait time.

Scenario: a webhook is processed twice and events arrive out of order. How do you make the handler robust?

I assume each message can arrive more than once and in any order. I check the signature. I save processed event IDs with a unique constraint. I make handlers idempotent. I use a state machine so impossible changes are ignored. When order matters, I ask the provider for the real current state. I reply fast and do the work in the background.

Scenario: a migration locked a hot table. How do you do it safely next time?

I use online methods. I use CREATE INDEX CONCURRENTLY. I add nullable columns and fill them in batches. I add constraints as not-validated and check them later. I set a lock timeout. I do migrations in expand-and-contract steps. I practise first on data as big as production.

Scenario: the job scheduler runs a task twice. What do you change?

I make sure only one instance runs it, with a distributed lock, an advisory lock or a managed scheduler. I also make the job idempotent, so a second run does no harm. I log run IDs and set alerts for duplicate or missed runs.

Scenario: after updating their profile a user still sees old data. Where do you look?

I look at every cache layer (browser, CDN, Redis) and at replica lag. I check the cache headers and keys. I check if a write clears or updates the cache. I check if the read hit a replica that was behind. To fix it, I clear the cache on write, use no-store for personal data, or read from the primary for a short time after a write.

Key takeaways

  • Most backend scenarios come from a few root causes: races, N+1 queries (one query for a list, then one more query for each item) and missing indexes, pools and timeouts, retries and idempotency, caching, migrations, key rotation and config differences.
  • Always answer in this order: Diagnose with named evidence, Fix with the smallest change, Prevent with a guard.
  • Idempotency, constraints, timeouts and observability appear in almost every "Prevent" line. Build them in early.

What's next

Lesson 12: Frontend and React Scenario Bank. The same practice for the browser side. It also covers design questions such as micro-frontends and design systems.

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

Keep learning

Related lessons

Related series