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:
- Symptom — what was reported, as vaguely as it usually is.
- First five minutes — the checks to run before forming opinions.
- Hypotheses — ranked, each with the cheap check that confirms or kills it.
- Root cause — the deepest thing that, once fixed, prevents the whole class of bug.
- Fix and prevention.
- 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 one | Typical bug |
|---|---|
| Configuration — env vars, secrets, feature flags | Missing or different value; value baked in at the wrong time |
| The build — dev server vs production build | Minification, tree-shaking, build-time vs runtime values, case-sensitive paths |
| The runtime — Node version, OS, browser engine | An API that exists on one and not the other |
| Time and place — time zone, locale, clock | Off-by-one dates, hydration mismatches, token expiry |
| Topology — one process locally vs many instances in production | State kept in one process's memory |
| Data — your small, clean test data vs real data | Nulls, huge records, old formats, unicode |
| History — a fresh page vs a tab open since yesterday | An 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.
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 read | Once, while the bundle is built | Every time the server starts (or every request) |
| Where it ends up | Inside the JavaScript file sent to every browser | In the server process's memory |
| Changing it requires | A new build | A restart or redeploy |
| Secret-safe? | No — anyone can read it in the bundle | Yes, 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:
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.
| Hypothesis | Cheap check | Result |
|---|---|---|
| The file is missing from the repo (not committed) | git ls-files components/ | grep -i avatar | The file is committed — as UserAvatar.tsx |
| Different Node or package versions in CI | Compare node -v and the lockfile | Same versions |
| The import's case differs from the file's case | Compare userAvatar with UserAvatar | Confirmed |
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"
"2026-10-10" (a plain date string) in the database.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.5in one locale is1.234,5in 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.
| Hypothesis | Cheap check | Result |
|---|---|---|
A random value or Date.now() in render | Search the component | Not used |
Invalid HTML nesting (<div> inside <p>) | Inspect the markup | Valid |
| Time zone or locale differs between server and browser | Compare the two strings | Confirmed — 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.
POST /logins1 in A's memory, sets cookie sid=s1First 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:
| Tell | Likely cause |
|---|---|
| Logged out after a deploy, every time | Sessions in memory are wiped when processes restart, or the session-signing secret changed |
| Only in Safari, or inside Instagram/Facebook in-app browsers | The session cookie is blocked as a third-party or cross-site cookie (SameSite, tracking prevention) |
A jwt not active / "token used before issued" error | Clock 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 another | The 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.
| Hypothesis | Cheap check | What confirms it |
|---|---|---|
| The production origin is missing from the API's allowlist | Compare the request's Origin with the allowlist | The 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 runs | curl -X OPTIONS with Origin and Access-Control-Request-Method | The OPTIONS response is 401/403 from auth middleware or a WAF, or 404/405 because nothing handles it |
| A server error is hiding behind CORS | Find the request in the API's logs by time | The API returned 500, and its error handler does not add CORS headers — so the browser reports CORS instead of the crash |
| Credentials with a wildcard | Read the response headers | Access-Control-Allow-Origin: * while the request sends cookies (credentials: 'include') — browsers reject that combination |
| A cache serves another origin's CORS response | Check Vary and the cache-status headers | A 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.
| Case | The difference | How it was reproduced |
|---|---|---|
| Production calls localhost | Build-time env at build time vs now | Searching the built bundle |
| Build fails only in CI | Case-insensitive vs case-sensitive file system | Reading the path literally |
| Dates a day early | User's time zone | DevTools time-zone override |
| Hydration error in production | Server zone vs browser zone | Running the local server with TZ=UTC |
| Random logouts | One process vs many instances | Following one user's requests across instances in the logs |
| Chunk load errors after deploy | Old page vs new deploy | Keeping a tab open across a deploy |
| CORS only in production | Permissive dev config or same-origin dev proxy vs the real policy | curl -X OPTIONS with the production Origin |
The Causal Chains
- 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
| Term | What it means |
|---|---|
| Environment parity | How closely development, CI and production match |
| Build-time variable | A value inlined into the bundle when it is built (NEXT_PUBLIC_*, VITE_*) |
| Runtime variable | A value the server reads from its environment when it runs |
| Inlining | Replacing a reference with its literal value during the build |
| Fail fast | Stopping immediately and loudly when something required is missing |
| Case-sensitive file system | One where User.ts and user.ts are different files (Linux) |
.nvmrc / engines | Ways to pin the Node version a project expects |
| Calendar date vs instant | A day on the calendar vs one specific moment in time |
TZ | The environment variable that sets a process's time zone |
| Locale | The language and regional formatting conventions for numbers and dates |
| Hydration mismatch | Server-rendered HTML differing from what the client renders |
| Stateless service | A service that keeps no per-user state in its own memory |
| Horizontal scaling | Running more instances of a service behind a load balancer |
| In-memory session store | Sessions kept inside one process — lost on restart, invisible to other instances |
| Sticky sessions | Load balancing that pins a user to one instance |
| Clock skew | Different machines' clocks disagreeing |
| NTP | The protocol that keeps machine clocks synchronised |
| Code splitting / chunk | Breaking a bundle into files loaded on demand |
| Content hash | A fingerprint of a file's contents in its filename |
| Version skew | Old clients running against a newer server or newer assets |
| Backward compatibility | A 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.