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.
blogs · jp-blogs.vercel.app/cheatsheets/series/debugging
The four moves and the first forkLesson 0 →
- Measure: what, how slow vs what, for whom, since when, what changed. Use p50, p95, p99, not averages.
- Isolate: which layer owns the time? Network, server, database, main thread, render.
- Hypothesize: one cause that can be proved wrong. Bisect, remove, compare fast with slow.
- Verify: change one thing, compare with the baseline, then add a guard (budget, test, alert).
| Network timing is large in | Meaning |
|---|---|
| DNS, connect, SSL | Distance, no keep-alive, many origins |
| Waiting (TTFB) | Backend: code, DB, external calls, cold start |
| Content download | Big payload, no compression |
API fast, UI slowLesson 1 →
| Suspect | Fix |
|---|---|
| Too many DOM nodes (tens of thousands) | Paginate, virtualise, content-visibility: auto |
| Huge payload, slow JSON.parse | Send fewer fields, Brotli, Web Worker, stream NDJSON |
| Re-render storm | Colocate state, memo, split context, selectors |
| Hydration cost | Server Components, code split, Suspense |
| Client waterfall | Promise.all, fetch on server, prefetch |
Slow endpoint (TTFB) and cachingLesson 2 →
| Suspect | Check | Fix |
|---|---|---|
| N+1 queries | Trace shows 100+ similar queries | include, join, IN, DataLoader |
| Missing index | EXPLAIN (ANALYZE, BUFFERS): Seq Scan | Composite index; CREATE INDEX CONCURRENTLY |
| Unbounded query | SELECT * with no limit | LIMIT, cursor, fewer columns |
| Serial external calls | Waterfall of spans | Promise.all, timeout, breaker |
| Pool exhausted | Pool wait, fast queries | Short 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.
- Colocate state: move it down into the small component that uses it.
- Pass children: elements made by the parent are not re-created.
- Split fast-changing parts from slow, expensive ones.
memowith stable props. It fails with new{},[]or() =>each render.useMemoanduseCallbackon purpose. Split contexts or use selectors.
Memory leaksLesson 4 →
| Source | Fix |
|---|---|
| addEventListener never removed | Remove in effect cleanup, same function reference |
| setInterval, subscriptions, sockets | Clear or unsubscribe on unmount |
| Detached DOM nodes | Drop references; destroy chart or map instances |
| Cache or array that only grows | LRU with max size or TTL |
- Performance monitor: healthy is a sawtooth, a leak is a staircase.
- Memory tab: force GC, snapshot 1, repeat the action 10 times, GC, snapshot 2.
- Compare; sort by retained size; filter "Detached". Counts that grow with the repeats are the leak.
- Read the Retainers chain, fix it, repeat and compare.
Async bugs in the frontendLesson 5 →
| Bug | Fix |
|---|---|
| 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 submit | Disable button, pending state; server Idempotency-Key plus unique constraint |
| Own app causes 429 | Dedupe, poll less, pause when hidden, back off, honour Retry-After |
| Stale closure in interval or handler | setCount(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 →
- Measure:
next experimental-analyze,source-map-explorer, Lighthouse. Quote parsed and compressed size. - Quick wins: remove dead deps,
lodash-essingle imports, drop duplicate versions, modernbrowserslist. - Split heavy parts (charts, editors, PDF) with dynamic
import(). Move formatting to Server Components. - Brotli, hashed files with
immutable, CDN. Lock in a budget (size-limit, Lighthouse CI).
| Hint | Use |
|---|---|
| preload | Needed now: LCP image, key font |
| prefetch | Likely next page, idle priority |
| preconnect | Critical third-party origin |
Long lists and tablesLesson 7 →
| Need | Choose |
|---|---|
| Huge, searchable, shareable pages | Server-side pagination |
| Skimmed feed | Infinite scroll with a cap |
| Big table or log, scrolled a lot | Virtualization with overscan |
| Long page that rarely changes | content-visibility: auto |
| Heavy sort or filter | Web Worker, or the server with indexes |
- Cost: set
aria-rowcountandaria-rowindex; Ctrl+F, focus and print only see drawn rows. - 50k-row sort freezes: virtualise,
memorows with stable keys, sort once inuseMemo, then worker or server.
Layout shift (CLS) and INPLesson 8 →
| CLS cause | Fix |
|---|---|
| Image or iframe with no size | width and height or aspect-ratio |
| Late ad, embed, banner | Reserve min-height; add below content |
| Font swap | font-display: swap plus matched fallback metrics |
| Animating top, height | Animate transform, opacity |
| INP cause | Fix |
|---|---|
| Heavy handler | Do the minimum, defer: startTransition, scheduler.yield() |
| Input re-renders page | Colocate state, useDeferredValue |
| Giant controlled form | Uncontrolled inputs, React Hook Form, validate on blur |
Production symptomsLesson 9 →
| Symptom | Likely cause and fix |
|---|---|
| CORS error in prod only | Exact origin allow-list; CORS before auth so the OPTIONS preflight gets 2xx |
| Random logouts | Session store TTL or eviction, refresh race (one refresh promise), different secrets |
| ChunkLoadError after deploy | Old chunks deleted: HTML no-cache, hashed names, keep last builds |
| Intermittent 502 or 504 | Node keepAliveTimeout above LB idle timeout (65 s vs 60 s) |
| Too many DB connections | Instances x pool size; use a pooler, one client per process |
| Charged twice | Idempotency key end to end, pass to provider, idempotent consumers |
Incident playbookLesson 10 →
- Declare early and assign roles: commander, investigator, communicator. Keep a timeline.
- Assess who, how badly, getting worse? Set a SEV level.
- Mitigate before diagnosing: roll back or flag off, then scale or shed load, fail over, isolate.
- Investigate: what changed? Dashboards (RED), deploys, errors, logs by request ID, traces, saturation.
- 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 →
| Symptom | Cause and fix |
|---|---|
| 500 for some users | Request ID, stack trace, what they share. Validate input, backfill data |
| Coupon used twice under load | Check-then-write race: atomic UPDATE, unique constraint, lock |
| Pool timeouts, DB CPU low | Long transactions; no network calls inside them |
| Webhook duplicated or out of order | Dedupe by event ID, idempotent handler, verify signature, reply 2xx fast |
| List slower deeper | Offset: switch to keyset with a composite index |
| Report endpoint times out | Return 202 with a job id; worker; poll |
| Migration froze the table | CONCURRENTLY, NOT VALID, lock_timeout = '3s' |
Frontend scenariosLesson 12 →
| Symptom | Cause and fix |
|---|---|
| List stale after a save | invalidateQueries, setQueryData, or revalidatePath |
| Slow on old phones | CPU throttle 4x to 6x; less JS, Server Components, lazy load |
| 5,000 updates a second | Batch and flush every 100 to 250 ms; latest per key; cap history |
| SPA not indexed, empty share | SSR or static, with title, canonical and Open Graph in HTML |
| Flaky E2E tests | Auto-waiting locators, own data per test, mock third parties, trace |
Security scenariosLesson 13 →
| Attack | Root fix |
|---|---|
| Stored XSS | Escape; allow-list sanitiser for HTML; check URLs; CSP |
| IDOR (/orders/124) | Scope every query to owner or tenant; return 404 |
| Password reset takeover | Single-use hashed token, short life, same reply for all emails, config base URL |
| CORS reflects any origin | Exact allow-list; no * with credentials; no endsWith |
| SQL injection | Parameterised queries everywhere |
| SSRF in link preview | Block private, loopback, link-local IPs; limit redirects; IMDSv2 |
| Leaked key in Git | Rotate first, then clean history |
Engineering judgementLesson 14 →
- Clarify goal and limits. List 2 or 3 options, including do nothing.
- Compare on risk, cost, time, ease to undo. Recommend one and say what would change your mind.
- 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 →
| Question | Short 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.