← All writing
Singy Songbook · 7 of 12
May 20, 2026 · 4 min read

When the Obstacle Isn't the Real Obstacle

A specific moment from the outage in post 01.

About 20 minutes in. The migration hang had moved from FastAPI's lifespan into a Python wrapper script. Different place, same hang. The script printed one line — "Will assume transactional DDL" — then froze.

What I wanted to do, viscerally, was keep tweaking the wrapper. The wrapper was the new thing; the hang must be because the wrapper was slightly wrong; just need a small fix.

That's the spelunking trap. Fifteen minutes into a hole, the next fifteen minutes always feel like they should be spent digging deeper.

What Claude said: "I've been spelunking too long in this direction. Let me sidestep by moving migrations out of Python entirely."

What "sidestep" meant

Delete the Python wrapper. Put the migration command directly in the Dockerfile CMD — a plain shell command that runs before Python ever starts.

Old approach: server runs Python, Python tries to run the migration, Python hangs.

New approach: server runs the migration as a plain shell command, waits for it to finish, then starts Python.

No Python. No async. No event loop. Migration ran in about two seconds. App came up. Outage over.

About 15 minutes from "let me sidestep" to "we're back." The 20 minutes before that were mostly wasted on the wrong question.

The cognitive move

The interesting thing isn't the technical fix — it's the pivot itself. Tried one strategy, noticed it wasn't working, named the failure, proposed a structurally different angle.

The default failure mode for both bad engineers and bad coding tools is to keep pushing on the thing that isn't working, making the fix more convoluted, refusing to admit the approach was wrong. "Let me try adding one more wrapper." "Let me try adjusting the timeout." "Let me try a different import order." Each plausible. Each wrong.

The memory file

Name the obstacle, propose a pivot. If you're stuck or a task
is harder than expected, say so clearly and offer a different
angle rather than grinding on more attempts. I'd rather hear
"I've been spelunking too long, let me sidestep by X" than
watch three more failed approaches.

It's paid off at least twice since.

A UI bug where the first three fixes didn't land. At the fourth, Claude paused: "I've been fighting the wrong layer of this component. Let me check whether the parent is passing the right props." The parent was the problem. The three fixes had been adjusting a component that wasn't the cause.

A deployment problem where an environment variable wasn't propagating. After two attempts at the config layer: "let me stop chasing the variable and read the actual container's environment." A preflight script was overriding it. Two minutes of observation saved an afternoon of guessing.

What I'm taking from this

The same trap exists when I'm debugging on my own. Ten minutes in, still insisting the bug is in the part of code I thought it was, when it's been obvious for eight minutes that it isn't. The move that helps: say it out loud — to Claude, explicitly. "I think I've been looking at the wrong layer. Let's reset and ask what else could cause this."

Saying it out loud is what breaks the loop. Claude becomes the audience I'm narrating to.

Takeaways

  • "I've been spelunking too long" is a useful sentence. Naming the loop is what breaks it.
  • The default failure mode (engineer or AI) is to keep tweaking the thing that isn't working. Make "name the obstacle, propose a pivot" an explicit rule.
  • The same trap happens in your own debugging. Saying it out loud, even just to a collaborator, surfaces the moment you've been ignoring.

Terms

FastAPI lifespan — FastAPI is the Python web framework the backend is built with. The "lifespan" is its startup-and-shutdown hooks: code that runs when the server boots and when it stops.

Migration — A script that changes the database schema. Run when the app deploys so the running code matches the database shape.

Python wrapper script — A small Python program written to glue together other tools. In this case, a script that called the migration tool programmatically and then started the web server.

Dockerfile — A text file describing how to build a container image: what to install, what files to copy, what command to run on startup.

CMD — The Dockerfile directive that specifies what command runs when the container starts.

Async / event loop — Python's mechanism for handling many requests concurrently in a single process. Code that's blocking (doesn't yield control) prevents other work from running.

Props — In React (the framework the frontend uses), the values a parent component passes down to a child. If the parent passes the wrong prop, the child renders incorrectly even if its own code is fine.

Container — An isolated package containing the backend code, its dependencies, and a small Linux environment. The hosting platform (Fly) runs this container.

Environment variable — A named value (like DATABASE_URL) the running process can read. Used for configuration that varies between environments — local, staging, production.

Preflight script — A small script that runs before the main application starts. Often used to set up environment variables, run health checks, or apply migrations.