DEV Community

David | Kordhub
David | Kordhub

Posted on

3 FastAPI Services, 1 Process: The Bugs Nobody Warns You About published: false tags: fastapi, python, api, webdev

I had three independent FastAPI services, each with its own repo and its own deployment, and I wanted to merge them into a single process — one app, three mounted sub-apps. On paper it's a five-line change: import each app, app.mount() it under a prefix, done.

In practice, it surfaced three bugs I wouldn't have hit any other way — and they're the kind of bugs that only show up once code that worked fine in isolation has to share a process with siblings it's never met.

The setup

Three independent APIs, each its own repo:

  • Discord Webhook Shield — stores Discord webhook URLs encrypted, proxies messages through with spam filtering and optional AI-based formatting
  • QR Code Generator — PNG/SVG generation with logo overlay
  • JSON Repair — fixes malformed JSON from LLM output, falling back to an AI repair pass when the deterministic parser can't handle it

Merging them meant mounting all three as FastAPI sub-applications under one root app:

from fastapi import FastAPI
from webhook_shield.main import app as webhook_shield_app
from qr_generator.main import app as qr_generator_app
from json_fixer.main import app as json_repair_app

app = FastAPI(title="Kordhub API Suite")
app.mount("/webhook-shield", webhook_shield_app)
app.mount("/qr", qr_generator_app)
app.mount("/json-repair", json_repair_app)
Enter fullscreen mode Exit fullscreen mode

Straightforward. Then I actually ran it.

Bug #1: a package shadowing its own dependency

The JSON repair service imports the json_repair library (from json_repair import repair_json). I'd named its own package folder json_repair too, for symmetry with the other two. Python resolved the local package first, and the import inside it silently broke — a name collision that only exists once you put sibling services in the same interpreter. Renamed the folder to json_fixer, problem gone. Obvious in hindsight, invisible until you actually boot the merged process.

Bug #2: .env files stomping on each other

Each service loaded its own .env the same way:

load_dotenv(dotenv_path=Path(__file__).resolve().parent / ".env")
Enter fullscreen mode Exit fullscreen mode

python-dotenv's load_dotenv() does not override a variable that's already set in the process environment — by default. That's fine when each service is its own process. It's not fine when three services share one process and all three .env files define a key with the same name (RAPIDAPI_PROXY_SECRET, in my case, each with a different value). Whichever module imports first "wins," and the other two silently validate against the wrong secret.

The minimal fix was override=True on each load_dotenv() call, so each module's own file takes precedence at the moment it loads. But that only solves it for local development, where each sub-package still has its own .env file. On a platform like Render, environment variables are injected once, process-wide — there's no such thing as three separate .env files anymore. So the real fix was to stop sharing the variable name at all: each sub-app now reads its own uniquely-named secret (WEBHOOK_SHIELD_PROXY_SECRET, QR_GENERATOR_PROXY_SECRET, JSON_FIXER_PROXY_SECRET), each still validated against the exact proxy secret RapidAPI already had configured for that specific API listing. No changes needed on RapidAPI's side, and the three services never have to agree on anything.

Bug #3: fail-fast code that fails the wrong thing

One of the services calls sys.exit(1) if a required credential is missing at import time — reasonable for a standalone service, since you want a fast, loud failure instead of serving broken requests. Reasonable, until it's one FastAPI sub-app mounted inside a bigger process: a missing variable now takes down all three APIs, not just the one that needed it. I left the fail-fast behavior in place (it's still the right call), but it's now a hard requirement that every environment variable for all three services be set together in the merged deployment — a constraint that didn't exist when they were separate.

What actually mattered

None of these were algorithm problems. They were all "two things that work fine alone don't automatically work fine together" problems — namespace collisions, shared process state, and failure modes that assume isolation. The lesson that generalizes: when you consolidate services that were never designed to share a process, test the combination, not just each piece. A health check on each mounted sub-app told me nothing about the env var collision — only calling a real authenticated endpoint on each one, with distinct test values, actually caught it.

The three APIs are live again, now sharing a single Render free-tier instance instead of three:

Curious how others have handled merging previously-independent services into one deployment — did you run into the same class of "works alone, breaks together" bugs, or something else entirely?

Top comments (0)