Weekend Build Log: Expiring Feature Flags That Cut Scope at Runtime A developer built a feature-flag system that embeds an expiry timestamp in each flag, so unfinished AI-backed features automatically return HTTP 503 with a fallback message once their deadline passes, rather than relying on a manual redeploy to disable them. The implementation uses frozen dataclasses with explicit UTC timestamps and a per-request dependency check with an injectable clock, and the accompanying test asserts that an expired flag returns 503 while never invoking the model. The approach moves scope-cut decisions into source control ahead of a weekend demo. A scope cut that lives in a planning document survives roughly four hours into a weekend build. A cut that lives in the request path survives the whole demo. This build log wires an expiry timestamp into every feature flag, so an unfinished feature stops answering requests on its own — no 1 a.m. redeploy, no verbal promise to remember to disable it. The demo is deliberately small: a text utility with three AI-backed features behind one free server instance. The constraint is the interesting part. Two days of calendar time, one process, and a model-call budget that should not be spent on a branch that is not finished. The artifact below has four moving parts: Feature record that carries expires at and an estimated model calls cost. 503 with a fallback message once the clock passes. A cut made on Saturday morning is a decision. A cut made on Sunday night is a negotiation with a tired person who wants the demo to look complete. The gap between those two states is where half-built features get flipped back on and quietly break the demo. Expiry moves the decision out of the tired person's hands. The feature is not "off for now"; it is "off after 21:00 UTC on Sunday," and that fact is in source control. The registry stays boring on purpose. Frozen dataclasses, explicit UTC timestamps, and a cost estimate that the planner can read later. python flags.py from dataclasses import dataclass from datetime import datetime, timezone from typing import Optional @dataclass frozen=True class Feature: name: str expires at: datetime UTC, hard stop fallback: Optional str = None model calls: int = 1 rough fan-out cost per request def open now self, now: Optional datetime = None - bool: return now or datetime.now timezone.utc < self.expires at DEMO END = datetime 2026, 10, 25, 21, 0, tzinfo=timezone.utc FEATURES = { f.name: f for f in Feature 'summary', DEMO END, model calls=1 , Feature 'compare', DEMO END, model calls=3 , Feature 'rewrite', datetime 2026, 10, 24, 12, 0, tzinfo=timezone.utc , fallback='rewrite is off for this build', model calls=1 , } Two details matter more than they look. model calls is an estimate, not a measurement, and it is only used to order what gets cut first. And open now takes an injectable clock, which is what makes the test in rule three possible. A flag read only at startup is a config file with extra steps. The check has to run per request, so a container that started at noon still refuses work at midnight. python api.py from fastapi import APIRouter, Depends, HTTPException from flags import FEATURES router = APIRouter def require name: str : def dependency : feature = FEATURES name if not feature.open now : raise HTTPException status code=503, detail={'feature': name, 'fallback': feature.fallback}, return feature return dependency @router.post '/api/summary' def summary : object = Depends require 'summary' : ... 503 is the honest status here. The route exists, the flag says no, and the client gets a fallback string it can render instead of a stack trace. Keeping the route mounted also means the frontend does not need a second code path for "feature missing." The useful assertion is not "the flag is closed." It is "the flag is closed and the model was never called." A closed flag that still bills a request is the failure mode worth testing for. python test flags.py from datetime import datetime, timezone from fastapi.testclient import TestClient from flags import FEATURES, Feature import main def test expired flag returns 503 and skips the model monkeypatch : calls = monkeypatch.setattr main, 'call model', lambda a, k: calls.append 1 or 'text' past = datetime 2026, 10, 20, 9, 0, tzinfo=timezone.utc monkeypatch.setitem FEATURES, 'summary', Feature 'summary', past client = TestClient main.app response = client.post '/api/summary', json={'text': 'hello'} assert response.status code == 503 assert response.json 'detail' 'feature' == 'summary' assert calls == Because require reads FEATURES name at call time rather than import time, monkeypatch.setitem is enough. No sleeping, no frozen-clock dependency, no flaky test on a slow laptop. Expiry only helps if it is checked before the code ships. A twenty-line script in the deploy path is cheaper than remembering. python check flags.py import sys from datetime import timedelta from flags import FEATURES, DEMO END GRACE = timedelta minutes=30 bad = f.name for f in FEATURES.values if f.expires at DEMO END + GRACE if bad: names = ', '.join bad print f'FAIL: flags outlive the demo window: {names}' sys.exit 1 print f'OK: {len FEATURES } flags expire inside the window' Run the whole loop locally in about a minute: python -m venv .venv && source .venv/bin/activate pip install fastapi uvicorn pytest httpx python check flags.py uvicorn main:app --port 8000 & curl -s -o /dev/null -w '%{http code}\n' -X POST localhost:8000/api/summary \ -H 'content-type: application/json' -d '{"text":"hello"}' A 200 means the flag is open, 503 means the cut already happened, and check flags.py failing means the cut was never scheduled. Ordering the cuts is the part people skip. The table below is the rule used for this build; the order is what matters, not the specifics. | Tier | Feature shape | Action at cutoff | |---|---|---| | 1 | Fans out to N model calls per request | Expire first, hard 503 , no fallback copy | | 2 | Single model call, partial UI | Expire mid-weekend, show fallback text | | 3 | No model call, pure formatting | Keep, no expiry | | 4 | Anything needing a schema change | Never scheduled | Tier 1 goes first because it is the only tier where a half-working feature can drain the request budget in minutes. Tier 3 has no marginal cost, so cutting it buys nothing. MonkeyCode offers free model access and a free server option, which is exactly the shape this harness assumes: one small process, outbound model calls, no control plane. The flags exist because model calls are the metered part. A route that fans out three calls per request is a different risk class from a route that formats text, and the registry should say so. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Two cautions before planning around any free tier. Free quotas, model lists, and instance terms change without notice, so confirm the current details on the project page rather than trusting a number from a blog post — including this one. And a free tier is not a reliability guarantee; the expiry design assumes the process can disappear at any moment, which is a useful assumption to hold even on paid infrastructure. If the demo shape fits, the free path is a reasonable place to run it before spending anything. Expiry in source code is not a feature-flag system. It has no audit log, no gradual rollout, no kill switch faster than a redeploy. FEATURES . The reason this pattern survived the weekend is that it made the boring decision once, in code, and then stopped asking. If a similar demo is coming up, start with check flags.py and write the expiry before the feature.