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.
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.
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.
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.
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.