22 min read

Chapter 11 — Casebook I: "It Works on My Machine"

Environment, Build, Time Zone, Session and Version Bugs — Investigated End to End

Part 3: The Casebook. Prerequisites: Parts 1 and 2 (Chapters 1–10).


How to Use the Casebook

Parts 1 and 2 gave you the method and the instruments. The casebook puts them to work on the bugs engineers meet again and again. Every case has the same shape:

  1. Symptom — what was reported, as vaguely as it usually is.
  2. First five minutes — the checks to run before forming opinions.
  3. Hypotheses — ranked, each with the cheap check that confirms or kills it.
  4. Root cause — the deepest thing that, once fixed, prevents the whole class of bug.
  5. Fix and prevention.
  6. What an interviewer is testing — because each of these is also a classic interview question.

Some cases are investigations you play. You see the symptom and several possible checks; pick one and you see what it would actually show. Dead ends cost time. Good checks move you forward. Try to finish each one on the shortest path — and when you are done, open "what the other checks would have found", because the dead ends teach as much as the right answers.

Why "it works on my machine" happens at all

"It works on my machine" is never a statement about the code alone. It is a statement that your environment and the failing environment differ, and the bug lives in one of the differences. This chapter's cases are all instances of the differential diagnosis from Chapter 1: list what differs, then eliminate.

What differs between your machine and the failing oneTypical bug
Configuration — env vars, secrets, feature flagsMissing or different value; value baked in at the wrong time
The build — dev server vs production buildMinification, tree-shaking, build-time vs runtime values, case-sensitive paths
The runtime — Node version, OS, browser engineAn API that exists on one and not the other
Time and place — time zone, locale, clockOff-by-one dates, hydration mismatches, token expiry
Topology — one process locally vs many instances in productionState kept in one process's memory
Data — your small, clean test data vs real dataNulls, huge records, old formats, unicode
History — a fresh page vs a tab open since yesterdayAn old version of the frontend talking to a new backend

Case 1 — "Production Calls localhost"

Symptom. A newly deployed frontend shows "Couldn't load your orders." It worked in local development and in yesterday's preview.

InvestigationTime spent: 0 min
Symptom
After today's production deploy, the orders page shows "Couldn't load your orders" for every user. It works in local development. No backend changes went out today.
1What do you check first?

What the case teaches

The key distinction is build-time configuration versus runtime configuration:

Build-time (e.g. NEXT_PUBLIC_*, VITE_*)Runtime (server-only env vars)
When it is readOnce, while the bundle is builtEvery time the server starts (or every request)
Where it ends upInside the JavaScript file sent to every browserIn the server process's memory
Changing it requiresA new buildA restart or redeploy
Secret-safe?No — anyone can read it in the bundleYes, if it never reaches the client

Two follow-on bugs from the same distinction:

  • The secret in the bundle. Someone prefixes an API secret with NEXT_PUBLIC_ "to make it work in the browser." It is now public, in every user's browser. Rotate it.
  • The silent fallback. ?? 'http://localhost:4000' turned a loud configuration error into a confusing runtime failure. Defaults for required configuration hide bugs; prefer failing fast.

What an interviewer is testing: whether you check what the browser actually sent before reading code, and whether you know where configuration lives at each stage of a build.


Case 2 — "The Build Fails Only in CI"

Symptom. npm run build succeeds on every developer's Mac. In CI (and on the hosting platform) it fails with:

text
Module not found: Can't resolve './components/userAvatar'

First five minutes. Read the error literally (Chapter 2): the path it cannot resolve is ./components/userAvatar. List the folder in the repository as CI sees it.

Hypotheses.

HypothesisCheap checkResult
The file is missing from the repo (not committed)git ls-files components/ | grep -i avatarThe file is committed — as UserAvatar.tsx
Different Node or package versions in CICompare node -v and the lockfileSame versions
The import's case differs from the file's caseCompare userAvatar with UserAvatarConfirmed

Root cause. macOS (and Windows) file systems are case-insensitive by default, so ./components/userAvatar finds UserAvatar.tsx. Linux file systems are case-sensitive, so the same import fails. CI and production run Linux. A common trigger: someone renames userAvatar.tsx to UserAvatar.tsx on a Mac, and Git does not record the case-only rename, so different machines disagree about the file's real name.

Fix. Make the import match the file exactly. To rename a file's case so Git records it: git mv userAvatar.tsx UserAvatar.tsx.

Prevention. Enable a lint rule or TypeScript's forceConsistentCasingInFileNames, and make sure CI runs the same build as production on every pull request.

The wider family. Other "builds on my machine" causes: a different Node version (pin it with an engines field or .nvmrc, and use the same version in CI); a package installed globally on your machine but missing from package.json; a devDependency used at runtime and missing when production installs only dependencies; a file ignored by .gitignore that exists only on your disk.

What an interviewer is testing: that you treat "only in CI" as a list of environment differences and check them, instead of retrying the build.


Case 3 — "Deadlines Are One Day Early for US Users"

InvestigationTime spent: 0 min
Symptom
Users in the United States report that task due dates show one day earlier than the date they picked. Users in India see the correct date. The date is stored as "2026-10-10" (a plain date string) in the database.
1What do you check first?

Time and place: the rest of the family

  • Dates vs instants. A birthday, a due date or a holiday is a calendar date — the same everywhere. A meeting start or a log timestamp is an instant — one moment, shown differently in each zone. Store and handle them as different things.
  • Server in UTC, laptop in IST. Code that formats dates on the server uses the server's zone (usually UTC), so "today" can be a different date on the server than for the user.
  • Locale. toLocaleString() without arguments formats numbers and dates differently per user: 1,234.5 in one locale is 1.234,5 in another; parsing user input with the wrong locale corrupts values.
  • Daylight saving time. An hour that happens twice (or not at all) once a year breaks schedules and "add 24 hours" logic. Chapter 14 covers time-based bugs in depth.

What an interviewer is testing: whether you reproduce by changing the environment to match the user's rather than asking for more screenshots — and whether you know the difference between a date and an instant.


Case 4 — The Hydration Error That Only Happens in Production

Symptom. After deploying, the error tracker shows Hydration failed because the server rendered HTML didn't match the client (or Minified React error #418) on the dashboard page. Nobody can reproduce it locally.

First five minutes. Production React errors are minified (Chapter 2): open the error's URL to get the full message, then look at which text mismatched. The error tracker's breadcrumb shows the server rendered Last updated 10/10/2026, 8:30:00 AM and the client rendered Last updated 10/10/2026, 2:00:00 PM.

Hypotheses.

HypothesisCheap checkResult
A random value or Date.now() in renderSearch the componentNot used
Invalid HTML nesting (<div> inside <p>)Inspect the markupValid
Time zone or locale differs between server and browserCompare the two stringsConfirmed — the server formatted in UTC, the browser in IST (UTC+5:30)

Why it works locally. On your laptop, the "server" (your dev server) and the browser run on the same machine, with the same time zone and locale, so both render the same string. In production, the server runs in UTC in a data centre and the browser runs wherever the user is. This is the time-and-place difference from Case 3, surfacing as a rendering bug.

Fix. Render values that depend on the viewer's time zone or locale only on the client (format after mount), or format them on the server in an explicit, fixed zone and locale so both sides agree. For a timestamp that must appear in the user's local time, rendering it in a client-only component is the honest fix.

Prevention. Run your local server with TZ=UTC to match production, so the mismatch appears on your machine first.

What an interviewer is testing: that you know what hydration is, why server and client must produce identical HTML, and which inputs can differ between them.


Case 5 — "Users Get Logged Out at Random"

Symptom. After the API was scaled from one instance to three, users report being logged out "randomly" — sometimes twice in a minute. It never happens locally.

#
Browser
Load balancer
Instance A
Instance B
1
POST /login
routes to A
creates session s1 in A's memory, sets cookie sid=s1
1 / 6

First five minutes. Logs filtered by one affected user's ID show 401 session not found — always on instance-b or instance-c, never on the instance where they logged in.

Root cause. The session store was the default in-memory store, which lives inside one process. Scaling out turned "one process holds all sessions" into "each process holds a third of them."

Fix. Move sessions to a shared store (Redis or the database), or use self-contained signed tokens that any instance can verify. Sticky sessions (the load balancer pins a user to one instance) hide the problem but break again whenever an instance restarts or scales down.

Other causes of random logouts, and their tells:

TellLikely cause
Logged out after a deploy, every timeSessions in memory are wiped when processes restart, or the session-signing secret changed
Only in Safari, or inside Instagram/Facebook in-app browsersThe session cookie is blocked as a third-party or cross-site cookie (SameSite, tracking prevention)
A jwt not active / "token used before issued" errorClock skew — a server's clock is behind the issuer's, so a fresh token looks like it is from the future. Sync clocks (NTP) and allow a small leeway
Logged out on one subdomain but not anotherThe cookie's Domain attribute does not cover both

What an interviewer is testing: stateless versus stateful services — the reason a system that works on one machine breaks when scaled horizontally.


Case 6 — "Failed to Fetch Dynamically Imported Module"

Symptom. Minutes after every deploy, the error tracker fills with ChunkLoadError: Loading chunk 482 failed or TypeError: Failed to fetch dynamically imported module. Reloading the page fixes it for that user. You never see it yourself.

First five minutes. Look at the failing URL: /_next/static/chunks/482.4f1c9a.js returns 404. The current deploy's chunk for that route is 482.b77e21.js.

Root cause — version skew. Modern bundles split code into chunks loaded on demand, with a content hash in each filename. A user who opened the app before the deploy has the old page in memory, which knows the old chunk names. When they navigate to a new route, the old page asks for an old chunk. If the deploy deleted the old files, the request 404s. You never see it because you always reload after deploying.

Fix and prevention.

  • Keep old assets available for a while after each deploy (immutable, versioned deployments do this automatically; self-hosted setups that overwrite one folder do not).
  • Recover gracefully: catch chunk-load failures and do a full page reload, once, so the user gets the new version.
  • Handle API skew too. The same old tab is also calling your API with old request shapes. Keep API changes backward compatible across at least one release, so old clients keep working until they reload.

What an interviewer is testing: that you think about two versions running at once — the normal state of every deployed web app for minutes or hours after a release.


Case 7 — "CORS Error Only in Production"

Symptom. The browser console shows Access to fetch at 'https://api.example.com/v1/orders' from origin 'https://www.example.com' has been blocked by CORS policy. Locally, with the frontend on localhost:3000 and the API on localhost:4000, everything works.

First five minutes. Do not touch CORS settings yet. Open the failing request in the Network panel (Chapter 5) and answer three questions: was there a preflight OPTIONS request, and what status did it get? What status did the real request get? Does the response carry an Access-Control-Allow-Origin header, and what value? Then reproduce the preflight with curl (Chapter 6) so the browser is out of the picture.

Hypotheses.

HypothesisCheap checkWhat confirms it
The production origin is missing from the API's allowlistCompare the request's Origin with the allowlistThe list has https://example.com but users are on https://www.example.com — or preview-deploy URLs were never added
The preflight is rejected before CORS runscurl -X OPTIONS with Origin and Access-Control-Request-MethodThe OPTIONS response is 401/403 from auth middleware or a WAF, or 404/405 because nothing handles it
A server error is hiding behind CORSFind the request in the API's logs by timeThe API returned 500, and its error handler does not add CORS headers — so the browser reports CORS instead of the crash
Credentials with a wildcardRead the response headersAccess-Control-Allow-Origin: * while the request sends cookies (credentials: 'include') — browsers reject that combination
A cache serves another origin's CORS responseCheck Vary and the cache-status headersA CDN cached a response with Allow-Origin: https://admin.example.com and serves it to www — the response lacks Vary: Origin (Chapter 12)

Why it works locally. In development, the API's CORS config often allows any origin, or a dev proxy makes the frontend and API the same origin, so CORS never applies. Production is the first place the real policy runs.

Fix. Whichever hypothesis was confirmed: add the exact production origins (including www, and a pattern for preview URLs if needed); handle OPTIONS before auth; add CORS headers to error responses too; return the exact origin with Allow-Credentials: true instead of *; add Vary: Origin when the allowed origin depends on the request.

What an interviewer is testing: that you know CORS is enforced by the browser on responses, that a CORS error can hide a real server error, and that you reproduce the preflight before changing configuration.


The Pattern Behind Every Case

Every case in this chapter was solved the same way: name the difference between the working and failing environments, then reproduce the failing environment on purpose.

CaseThe differenceHow it was reproduced
Production calls localhostBuild-time env at build time vs nowSearching the built bundle
Build fails only in CICase-insensitive vs case-sensitive file systemReading the path literally
Dates a day earlyUser's time zoneDevTools time-zone override
Hydration error in productionServer zone vs browser zoneRunning the local server with TZ=UTC
Random logoutsOne process vs many instancesFollowing one user's requests across instances in the logs
Chunk load errors after deployOld page vs new deployKeeping a tab open across a deploy
CORS only in productionPermissive dev config or same-origin dev proxy vs the real policycurl -X OPTIONS with the production Origin

The Causal Chains

Self-test: predict each consequence before revealing it.
  • If the production bundle contains a value that is wrong or missing
  • If you change a NEXT_PUBLIC_ variable and restart without rebuilding
  • If required configuration has a silent default like localhost
  • If an import works on macOS but fails in CI
  • If dates are a day early only for users west of UTC
  • If a bug appears only for users in another time zone
  • If a hydration mismatch shows only in production
  • If users are logged out at random after scaling out
  • If chunk-load errors appear right after each deploy
  • If a CORS error appears only in production
  • If something works on one machine and fails on another

The Rule You Keep Forever

"It works on my machine" means "my machine is different." Find the difference — configuration, build, runtime, time zone, topology, data or version — and make your machine fail the same way before you change any code.


Vocabulary Map

TermWhat it means
Environment parityHow closely development, CI and production match
Build-time variableA value inlined into the bundle when it is built (NEXT_PUBLIC_*, VITE_*)
Runtime variableA value the server reads from its environment when it runs
InliningReplacing a reference with its literal value during the build
Fail fastStopping immediately and loudly when something required is missing
Case-sensitive file systemOne where User.ts and user.ts are different files (Linux)
.nvmrc / enginesWays to pin the Node version a project expects
Calendar date vs instantA day on the calendar vs one specific moment in time
TZThe environment variable that sets a process's time zone
LocaleThe language and regional formatting conventions for numbers and dates
Hydration mismatchServer-rendered HTML differing from what the client renders
Stateless serviceA service that keeps no per-user state in its own memory
Horizontal scalingRunning more instances of a service behind a load balancer
In-memory session storeSessions kept inside one process — lost on restart, invisible to other instances
Sticky sessionsLoad balancing that pins a user to one instance
Clock skewDifferent machines' clocks disagreeing
NTPThe protocol that keeps machine clocks synchronised
Code splitting / chunkBreaking a bundle into files loaded on demand
Content hashA fingerprint of a file's contents in its filename
Version skewOld clients running against a newer server or newer assets
Backward compatibilityA new version still accepting what old clients send

Practice Problems

Problem 1 — List the differences. A PDF export works locally but produces a blank file in production. Write down at least six differences between your laptop and the production container that could explain it, and the cheapest check for each.

Problem 2 — Build or runtime? For each value, say whether it should be build-time or runtime configuration, and why: (a) the public analytics key; (b) the database password; (c) the API base URL a browser calls; (d) a feature flag you want to flip without redeploying.

Problem 3 — The date bug. A booking page shows "Fri 10 Oct" for an Indian user and "Thu 9 Oct" for a Brazilian user, for the same booking. The API returns "2026-10-10". Write the one-line explanation, the two-minute reproduction, and the fix.

Problem 4 — Scaling out. A service works on one instance. List three kinds of state (other than sessions) that commonly live in process memory and break when you run three instances, and where each should move.

Problem 5 — Two versions at once. You are about to rename a field in an API response from userName to displayName. Old browser tabs will keep calling the API for hours after the deploy. Describe a rollout that never breaks either old or new clients.


What's Coming Next

Many of these cases had a cache somewhere in the story: a bundle cached with the old value, an old page cached in a tab. Chapter 12 is entirely about caches — the single most common source of "it's fixed but users still see the bug," of data shown to the wrong user, and of the outage that starts the moment a cache is emptied.