Can CeylanVienna-based, globally curious.
Learn/Architecture

When the bug isn't a bug: diagnosing runtime barriers before debugging

Some failures aren't bugs in your code — they're structural mismatches between your runtime and the capability you're trying to use. Recognising this distinction saves days of misdirected debugging.

2026-04-18·3 min read·intermediate

The pattern that wastes days

You need a capability — web scraping, image processing, ML inference. You reach for your existing stack. You try library A, it fails. You try library B, same class of error. You try a workaround, it partially works. You try another workaround. Three days later you have a brittle solution held together with patches.

The diagnosis that would have saved those three days: the runtime is wrong for this capability. No amount of library-switching or workaround-stacking will produce a clean solution, because the underlying problem is structural.

This is a runtime barrier — a mismatch between what your current environment can do well and what you're asking it to do.

The signal

A runtime barrier looks like:

  • The same error class persists across multiple different libraries
  • Workarounds work partially but introduce new problems
  • The error occurs at a level below your code (TLS, native module, OS)
  • The ecosystem for this capability is thin or unmaintained in your language

Common examples:

Capability Poor-fit runtime Symptom
Web scraping with anti-bot Node.js 403s or empty results that Python handles fine
ML inference Node.js / Go No native tensor runtime; everything is a wrapper
Heavy parallel computation Python (GIL) CPU-bound tasks don't parallelise
Reactive UI Python No native component model; everything is a workaround

The diagnosis process

Step 1: Name the capability precisely. Not "it's not working" — "I'm trying to make authenticated HTTP requests that bypass bot detection." Precise naming lets you assess fit against known ecosystem strengths.

Step 2: Check the ecosystem. Search for the 3 most popular libraries for this capability in your runtime. If they all have the same class of failure or are unmaintained, that's an ecosystem gap, not a library bug.

Step 3: Cross-reference with a reference runtime. Does the capability work cleanly in another language? If Python's requests + anti-bot library handles this in 10 lines, and Node.js has no equivalent after 3 library attempts, the gap is real.

Step 4: State the barrier clearly. "This is a runtime barrier. Node.js is the wrong tool for anti-bot scraping. No amount of debugging will fix this — the ecosystem gap is fundamental."

This is a hard sentence to say, especially after investment in a particular approach. It's also the sentence that unblocks progress.

The architectural response

Once you've diagnosed a barrier, the solution is a service boundary — not a workaround.

A service boundary means: let each capability live in the runtime best suited to it, and define a clean interface between them.

Option A — separate process: The capability runs as a standalone process in the right runtime. It writes results to a shared database or communicates via HTTP. Your main application reads from the database. No shared runtime, no compromise.

Option B — purpose-built script: For scheduled or batch work, a standalone script in the right language is called by your scheduler. It doesn't live in your main application at all.

What you don't do: embed the capability as a subprocess call (python script.py from Node.js, exec(), shell-out). This creates two runtimes with two package managers, two test runners, and deployment confusion. It looks like a solution and is actually a maintenance problem.

Document the barrier

Once a runtime barrier is diagnosed and resolved, document it:

## Runtime Barrier — [date]

**Capability:** Anti-bot web scraping
**Runtime attempted:** Node.js
**Failure:** All tested libraries produced empty results against DataDome protection
**Resolution:** Python scraper process writes to shared SQLite; Node.js reads from it
**Do not re-attempt:** Node.js scraping for this target — the barrier is structural

This entry prevents a future developer (or a future version of yourself) from re-attempting the same failed approach and re-discovering the same barrier from scratch.

More like this, straight to your inbox.

I write about Architecture and a handful of other things I actually care about. No schedule, no filler — just when I have something worth saying.

More on Architecture

Preventing a single channel from becoming the accidental default in multi-channel systems

In multi-platform publishing and notification systems, whichever channel was implemented first tends to become the silent default. Other channels get skipped without error. The fix is making required outputs explicit from the start — not implicit from what exists.

Soft deletes aren't just for audit trails — they're your sales pipeline

When you hard-delete a record, you lose the sales lead. Inactive records in a marketplace platform are your best prospects — enforce soft delete at the database role level, not in application code.

Catch data conflicts before code review, not after

Two features touching the same database table is a conflict waiting to happen. A feature registry and a mandatory conflict check forces the conversation before the code is written.

If this raised a question, I'd be happy to talk about it.

Find me →
← Back to Learn