Cheat sheet · Scenario Debugging: The “It's Slow, Fix It” Guide

Scenario Debugging cheat sheet

Symptom, likely cause, how to check, fix. Always measure and isolate before you change code, and finish with a guard so the problem does not return.

Read the full series →

blogs · jp-blogs.vercel.app/cheatsheets/series/debugging

The four moves and the first forkLesson 0 →

  1. Measure: what, how slow vs what, for whom, since when, what changed. Use p50, p95, p99, not averages.
  2. Isolate: which layer owns the time? Network, server, database, main thread, render.
  3. Hypothesize: one cause that can be proved wrong. Bisect, remove, compare fast with slow.
  4. Verify: change one thing, compare with the baseline, then add a guard (budget, test, alert).
Network timing is large inMeaning
DNS, connect, SSLDistance, no keep-alive, many origins
Waiting (TTFB)Backend: code, DB, external calls, cold start
Content downloadBig payload, no compression

API fast, UI slowLesson 1 →

SuspectFix
Too many DOM nodes (tens of thousands)Paginate, virtualise, content-visibility: auto
Huge payload, slow JSON.parseSend fewer fields, Brotli, Web Worker, stream NDJSON
Re-render stormColocate state, memo, split context, selectors
Hydration costServer Components, code split, Suspense
Client waterfallPromise.all, fetch on server, prefetch

Slow endpoint (TTFB) and cachingLesson 2 →

SuspectCheckFix
N+1 queriesTrace shows 100+ similar queriesinclude, join, IN, DataLoader
Missing indexEXPLAIN (ANALYZE, BUFFERS): Seq ScanComposite index; CREATE INDEX CONCURRENTLY
Unbounded querySELECT * with no limitLIMIT, cursor, fewer columns
Serial external callsWaterfall of spansPromise.all, timeout, breaker
Pool exhaustedPool wait, fast queriesShort transactions, pooler
  • Cache last. Cache-aside with a TTL, explicit delete on write, or versioned keys (product:v2:id).
  • Stampede: single flight, a short lock, jittered TTLs, stale-while-revalidate.

Re-rendersLesson 3 →

  • A component re-renders when its state changes, its parent renders, a context it reads changes, or its store slice changes.
  • Profile first: React DevTools Profiler, "why did this render", highlight updates.
  1. Colocate state: move it down into the small component that uses it.
  2. Pass children: elements made by the parent are not re-created.
  3. Split fast-changing parts from slow, expensive ones.
  4. memo with stable props. It fails with new {}, [] or () => each render.
  5. useMemo and useCallback on purpose. Split contexts or use selectors.

Memory leaksLesson 4 →

SourceFix
addEventListener never removedRemove in effect cleanup, same function reference
setInterval, subscriptions, socketsClear or unsubscribe on unmount
Detached DOM nodesDrop references; destroy chart or map instances
Cache or array that only growsLRU with max size or TTL
  1. Performance monitor: healthy is a sawtooth, a leak is a staircase.
  2. Memory tab: force GC, snapshot 1, repeat the action 10 times, GC, snapshot 2.
  3. Compare; sort by retained size; filter "Detached". Counts that grow with the repeats are the leak.
  4. Read the Retainers chain, fix it, repeat and compare.

Async bugs in the frontendLesson 5 →

BugFix
Effect fetches twice in dev (Strict Mode)Make it idempotent with cleanup. Do not remove Strict Mode
Old response overwrites new (race)AbortController in cleanup, or an ignore flag, or React Query or SWR
Double submitDisable button, pending state; server Idempotency-Key plus unique constraint
Own app causes 429Dedupe, poll less, pause when hidden, back off, honour Retry-After
Stale closure in interval or handlersetCount(c => c + 1), refs, useEffectEvent, right deps
useEffect(() => {
  const c = new AbortController();
  fetch(url, { signal: c.signal })
    .then(r => r.json()).then(setData)
    .catch(e => {
      if (e.name !== "AbortError") setErr(e);
    });
  return () => c.abort();
}, [url]);

Debounce cuts the number of requests. Abort makes the latest one win.

Bundle size and LCPLesson 6 →

  1. Measure: next experimental-analyze, source-map-explorer, Lighthouse. Quote parsed and compressed size.
  2. Quick wins: remove dead deps, lodash-es single imports, drop duplicate versions, modern browserslist.
  3. Split heavy parts (charts, editors, PDF) with dynamic import(). Move formatting to Server Components.
  4. Brotli, hashed files with immutable, CDN. Lock in a budget (size-limit, Lighthouse CI).
HintUse
preloadNeeded now: LCP image, key font
prefetchLikely next page, idle priority
preconnectCritical third-party origin

Long lists and tablesLesson 7 →

NeedChoose
Huge, searchable, shareable pagesServer-side pagination
Skimmed feedInfinite scroll with a cap
Big table or log, scrolled a lotVirtualization with overscan
Long page that rarely changescontent-visibility: auto
Heavy sort or filterWeb Worker, or the server with indexes
  • Cost: set aria-rowcount and aria-rowindex; Ctrl+F, focus and print only see drawn rows.
  • 50k-row sort freezes: virtualise, memo rows with stable keys, sort once in useMemo, then worker or server.

Layout shift (CLS) and INPLesson 8 →

CLS causeFix
Image or iframe with no sizewidth and height or aspect-ratio
Late ad, embed, bannerReserve min-height; add below content
Font swapfont-display: swap plus matched fallback metrics
Animating top, heightAnimate transform, opacity
INP causeFix
Heavy handlerDo the minimum, defer: startTransition, scheduler.yield()
Input re-renders pageColocate state, useDeferredValue
Giant controlled formUncontrolled inputs, React Hook Form, validate on blur

Production symptomsLesson 9 →

SymptomLikely cause and fix
CORS error in prod onlyExact origin allow-list; CORS before auth so the OPTIONS preflight gets 2xx
Random logoutsSession store TTL or eviction, refresh race (one refresh promise), different secrets
ChunkLoadError after deployOld chunks deleted: HTML no-cache, hashed names, keep last builds
Intermittent 502 or 504Node keepAliveTimeout above LB idle timeout (65 s vs 60 s)
Too many DB connectionsInstances x pool size; use a pooler, one client per process
Charged twiceIdempotency key end to end, pass to provider, idempotent consumers

Incident playbookLesson 10 →

  1. Declare early and assign roles: commander, investigator, communicator. Keep a timeline.
  2. Assess who, how badly, getting worse? Set a SEV level.
  3. Mitigate before diagnosing: roll back or flag off, then scale or shed load, fail over, isolate.
  4. Investigate: what changed? Dashboards (RED), deploys, errors, logs by request ID, traces, saturation.
  5. Communicate often. Verify in metrics. Write a blameless postmortem: prevent, detect, mitigate items.
10x traffic
Cache or stale, scale out, rate limit with 429, degrade, block abuse
Cascade from slow dependency
Breaker, short timeouts, bulkhead, flag it off
Queue backing up
Poison message to DLQ, fix consumer, scale

Backend scenariosLesson 11 →

SymptomCause and fix
500 for some usersRequest ID, stack trace, what they share. Validate input, backfill data
Coupon used twice under loadCheck-then-write race: atomic UPDATE, unique constraint, lock
Pool timeouts, DB CPU lowLong transactions; no network calls inside them
Webhook duplicated or out of orderDedupe by event ID, idempotent handler, verify signature, reply 2xx fast
List slower deeperOffset: switch to keyset with a composite index
Report endpoint times outReturn 202 with a job id; worker; poll
Migration froze the tableCONCURRENTLY, NOT VALID, lock_timeout = '3s'

Frontend scenariosLesson 12 →

SymptomCause and fix
List stale after a saveinvalidateQueries, setQueryData, or revalidatePath
Slow on old phonesCPU throttle 4x to 6x; less JS, Server Components, lazy load
5,000 updates a secondBatch and flush every 100 to 250 ms; latest per key; cap history
SPA not indexed, empty shareSSR or static, with title, canonical and Open Graph in HTML
Flaky E2E testsAuto-waiting locators, own data per test, mock third parties, trace

Security scenariosLesson 13 →

AttackRoot fix
Stored XSSEscape; allow-list sanitiser for HTML; check URLs; CSP
IDOR (/orders/124)Scope every query to owner or tenant; return 404
Password reset takeoverSingle-use hashed token, short life, same reply for all emails, config base URL
CORS reflects any originExact allow-list; no * with credentials; no endsWith
SQL injectionParameterised queries everywhere
SSRF in link previewBlock private, loopback, link-local IPs; limit redirects; IMDSv2
Leaked key in GitRotate first, then clean history

Engineering judgementLesson 14 →

  1. Clarify goal and limits. List 2 or 3 options, including do nothing.
  2. Compare on risk, cost, time, ease to undo. Recommend one and say what would change your mind.
  3. Roll out in small steps with a rollback and a measure. Tell stakeholders outcomes and risks.
Monolith or microservices?
Fix tests, CI, flags first; modular monolith; extract with strangler
Rewrite or refactor?
Strangler fig plus characterisation tests
Rename a column, zero downtime
Expand, dual-write, batch backfill, verify, switch reads, drop later
Multi-tenant leaks
Tenant id in every query, RLS, tenant from the session, cross-tenant tests

Interview one-linersLesson 0 →

QuestionShort answer
"The page is slow." First move?Measure, then split network, server and main thread. No useMemo or Redis yet.
API fast, UI slow?Main-thread work: DOM size, renders, parsing, hydration.
Typing shows old results?Race: abort the previous request, debounce, key by query.
Incident at 3 a.m.?Acknowledge, what changed, scope, roll back, communicate, postmortem.

A cheat sheet is a reminder, not a teacher. Each card links to the lesson that explains it step by step.