What your local environment hides
Concurrency, latency, data volume and partial failures, the four things a local environment hides.
The problem
Section titled “The problem”“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 insertapp.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 atomicallyapp.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:
-- ✅ atomicUPDATE 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 20const 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);2. Latency: every round trip costs
Section titled “2. Latency: every round trip costs”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 ownerconst 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];}// ✅ A single query, with a JOINconst 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.
3. Data: volume and mess
Section titled “3. Data: volume and mess”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 behaveINSERT 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.
Pitfalls
Section titled “Pitfalls”- 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.
- DocsTransaction IsolationPostgreSQL
What each isolation level guarantees and which concurrency anomalies it allows.
- DocsPostgreSQL Error CodesPostgreSQL
The list of error codes, including the 23505 (unique_violation) used in this lesson.
- BookUse The Index, LukeMarkus Winand
Free online. How indexes work and why queries slow down as data grows.
- ArticleFallacies of distributed computingWikipedia
The eight false assumptions about networks that a local environment reinforces.
- Docs