Skip to content
M01Local is not productionLesson 3 of 4

What your local environment hides

Concurrency, latency, data volume and partial failures, the four things a local environment hides.

Goal
Recognize the four conditions a local environment doesn’t reproduce and test each one before production tests it for you.

“Works on my machine” is usually true. It just holds for an environment with one user, zero latency, little data and nothing failing. Production has the opposite of all four, and each one hides a whole class of bugs.

1. Concurrency: two users at the same time

Section titled “1. Concurrency: two users at the same time”

Locally, you click once and wait for the response. In production, two requests of the same kind arrive in the same millisecond, on different instances.

The dangerous pattern is check-then-act:

// ❌ Race condition: both requests pass the check before either insert
app.post('/api/links', async (req, res) => {
const { slug, url } = req.body;
const existing = await db.query('SELECT 1 FROM links WHERE slug = $1', [slug]);
if (existing.rowCount > 0) return res.status(409).json({ error: 'Slug already taken' });
// ...between the SELECT and the INSERT, another request may have inserted the same slug
await db.query('INSERT INTO links (slug, url, user_id) VALUES ($1, $2, $3)', [slug, url, req.user.id]);
res.status(201).end();
});
sequenceDiagram
  participant A as Request A
  participant DB as Postgres
  participant B as Request B
  A->>DB: SELECT slug='promo' → nothing
  B->>DB: SELECT slug='promo' → nothing
  A->>DB: INSERT 'promo' ✔
  B->>DB: INSERT 'promo' ✔ (duplicate!)

The fix is to let the database enforce the rule, because it’s the only place that sees every request:

ALTER TABLE links ADD CONSTRAINT links_slug_key UNIQUE (slug);
// ✅ Insert directly; the database rejects duplicates atomically
app.post('/api/links', async (req, res) => {
const { slug, url } = req.body;
try {
await db.query('INSERT INTO links (slug, url, user_id) VALUES ($1, $2, $3)', [slug, url, req.user.id]);
res.status(201).end();
} catch (err: any) {
if (err.code === '23505') return res.status(409).json({ error: 'Slug already taken' }); // unique_violation
throw err;
}
});

Counters work the same way. clicks = clicks + 1 run in the database is atomic. Reading the value into JavaScript, adding one and writing it back loses increments under load:

-- ✅ atomic
UPDATE links SET clicks = clicks + 1 WHERE id = $1;

How to test it: fire several identical requests at once and count how many succeeded.

// scripts/race.ts: should print 1 success, not 20
const results = await Promise.all(
Array.from({ length: 20 }, () =>
fetch('http://localhost:3000/api/links', {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: `Bearer ${TOKEN}` },
body: JSON.stringify({ slug: 'promo', url: 'https://example.com' }),
}),
),
);
console.log('successes:', results.filter((r) => r.status === 201).length);

Locally, a query goes there and back in ~0.1 ms. In production, the database is on another machine, 1 or 2 ms away. That sounds like nothing until you multiply it.

The classic case is N+1: one query for the list and one more for each item.

// ❌ 1 query for the links + 1 per link to fetch the owner
const links = await db.query('SELECT * FROM links ORDER BY created_at DESC LIMIT 100');
for (const link of links.rows) {
link.owner = (await db.query('SELECT name FROM users WHERE id = $1', [link.user_id])).rows[0];
}
Same N+1 code (101 queries), different environments
Local (0.1 ms/query)10 ms
Production, same region (1.5 ms)152 ms
Database in another region (40 ms)4,040 ms

Network time only, not counting execution. Locally the problem is invisible.

Show data
Local (0.1 ms/query)10 ms
Production, same region (1.5 ms)152 ms
Database in another region (40 ms)4,040 ms
// ✅ A single query, with a JOIN
const links = await db.query(`
SELECT l.id, l.slug, l.url, u.name AS owner_name
FROM links l
JOIN users u ON u.id = l.user_id
ORDER BY l.created_at DESC
LIMIT 100
`);

ORMs hide this pattern: touching link.owner inside a loop can fire one query per item without you noticing. Turn on query logging in development and count how many queries each route makes.

A query without an index on a 500-row table takes 1 ms. With 10 million rows it takes seconds, and the slowdown doesn’t creep up at a pace you can react to. The problem shows up all at once, on a peak day.

Besides volume, real data is messy: NULL where you didn’t expect it, names with emoji, 4,000-character URLs, mixed time zones, records from before the migration that added the column.

Generating volume locally takes a minute:

-- 1 million fake links to see how your queries behave
INSERT INTO links (user_id, slug, url, created_at)
SELECT
(random() * 10000)::int + 1,
md5(g::text),
'https://example.com/' || g,
now() - random() * interval '365 days'
FROM generate_series(1, 1000000) AS g;

Then run EXPLAIN ANALYZE on your main queries. Reading the plan is a topic for M11; for now, look for Seq Scan on large tables.

4. Partial failures: something is always broken

Section titled “4. Partial failures: something is always broken”

Locally, either everything works or nothing does. In production, parts fail:

  • The email API responds in 30 s instead of 300 ms, and your requests sit there waiting.
  • An instance restarts in the middle of an operation, and whatever was in its memory is gone.
  • The network loses the response, but the operation ran. The client tries again and it runs twice.

Each of these gets its own module: state and idempotency in M02, timeouts and retries in M17. In production, plan for failure from the start and ask what happens when it comes.

  • Testing only the happy path with one user. The most expensive bugs live in concurrency and failure.
  • Trusting the front end to enforce rules. Disabling the button doesn’t stop two requests. The rule has to live on the server, and preferably in the database.
  • What AI tends to generate: check-then-act in almost every create endpoint, queries inside loops and no constraint in the schema besides the primary key.

Go deeper

Materials in English unless noted.