Application vs. Build Lifecycles
Understanding the difference between the Application Lifecycle and the Build Lifecycle is critical for maintaining stable, performant backend systems.
The Application Lifecycle (Runtime)
The Application Lifecycle refers to the events that happen while the server is running. In FastAPI, this is managed by the @asynccontextmanager lifespan hook:
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup logic: connect to DB, load ML models into memory
yield
# Shutdown logic: close DB connections, clear cache
These tasks are tied strictly to the runtime memory and health of the API.
The Build Lifecycle (Compile-Time)
The Build Lifecycle refers to tasks that must happen before the application starts, or as external side-effects during development. Examples include: - Compiling CSS (e.g., Tailwind) - Bundling JavaScript (e.g., Webpack, Vite) - Running database migrations (e.g., Alembic)
The Danger of Mixing Them
A common, dangerous anti-pattern is running compile-time tasks inside the runtime lifecycle:
# ❌ ANTI-PATTERN: Don't do this!
@asynccontextmanager
async def lifespan(app: FastAPI):
process = subprocess.Popen(["tailwindcss", "--watch"])
yield
process.terminate()
Why is this bad?
- Memory Overhead: Running a Node.js/Shell compiler alongside a Python production server consumes unnecessary RAM.
- Orphaned Processes: If the Python server crashes or
process.terminate()fails, the background watcher keeps running, creating a "zombie" process. - Immutability: In production (like Docker containers), the filesystem should ideally be immutable. Compiling files at runtime violates this principle.
The Correct Approach
Extract the Build Lifecycle!
- In Development: Run the watcher as a completely separate background process (e.g., in a separate
tmuxwindow or terminal tab) managed by aMakefile. - In Production: Run the compiler exactly once during the CI/CD pipeline or Docker build stage. The application server should only serve the final, static output files.