Serving real-time rate recommendations via FastAPI
The read path for pricing has a brutal contract: answer in a few milliseconds, never publish a stale rate, and let the caller reconstruct exactly which decision produced the number. This guide builds that endpoint with FastAPI, implementing the serving layer described in Price Recommendation Serving within the Dynamic Pricing Rule Engines & Optimization pillar. The result is a cache-backed API that fails loud on staleness and stamps every response with the engine version.
Prerequisites
- Python 3.11+
fastapiand an ASGI server such asuvicorn- A cache populated by the pricing engine’s write path (in-process dict here; Redis in production)
- Rates in integer minor units and a canonical currency, per Rate Plan Structuring & Mapping
- A freshness SLA (TTL) agreed with the consuming Channel Manager Integration Patterns team
Step 1 — Define the recommendation record and cache
The served object is deliberately small and fully self-describing: rate, currency, when it was computed, and which engine version produced it. The computed-at timestamp is what lets the read path enforce freshness.
from __future__ import annotations
import logging
import time
from dataclasses import dataclass
logger = logging.getLogger("pricing.serving")
@dataclass(frozen=True)
class Recommendation:
rate_cents: int
currency: str
computed_at: float # time.monotonic() at write
engine_version: str
_CACHE: dict[tuple[str, str, str], Recommendation] = {}
_TTL_SECONDS = 900
def put(property_id: str, room_type: str, stay_date: str,
rec: Recommendation) -> None:
_CACHE[(property_id, room_type, stay_date)] = rec
Step 2 — Serve reads, failing loud on staleness
The endpoint returns the rate only when it is within the freshness SLA. A cache miss is a 404; a stale entry is a 503, signaling the caller to fall back or retry rather than publish an old price.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/rate/{property_id}/{room_type}/{stay_date}")
def get_rate(property_id: str, room_type: str, stay_date: str) -> dict:
rec = _CACHE.get((property_id, room_type, stay_date))
if rec is None:
logger.warning("miss %s/%s/%s", property_id, room_type, stay_date)
raise HTTPException(status_code=404, detail="no recommendation")
age = time.monotonic() - rec.computed_at
if age > _TTL_SECONDS:
logger.error("stale %s/%s/%s age=%.0fs", property_id, room_type, stay_date, age)
raise HTTPException(status_code=503, detail="recommendation stale")
return {
"rate_cents": rec.rate_cents,
"currency": rec.currency,
"engine_version": rec.engine_version,
"age_seconds": round(age),
}
Step 3 — Add a health and freshness probe
Serving infrastructure needs a way for orchestration to know it is healthy and fresh. A liveness check that returns 200 while the cache is full of stale rates is worse than useless; expose freshness explicitly.
@app.get("/healthz")
def healthz() -> dict:
now = time.monotonic()
total = len(_CACHE)
stale = sum(1 for r in _CACHE.values() if now - r.computed_at > _TTL_SECONDS)
status = "degraded" if total and stale / total > 0.1 else "ok"
return {"status": status, "cached": total, "stale": stale}
Reporting degraded when more than ten percent of entries are stale turns a silent recompute backlog into an actionable signal your dashboards and alerts can key on.
Verification and testing
FastAPI’s TestClient drives the endpoint end to end, including the staleness path, by writing an entry with a backdated timestamp.
from fastapi.testclient import TestClient
def test_stale_entry_returns_503() -> None:
client = TestClient(app)
put("h1", "suite", "2026-08-01",
Recommendation(rate_cents=22000, currency="USD",
computed_at=time.monotonic() - 10_000, # older than TTL
engine_version="v9"))
resp = client.get("/rate/h1/suite/2026-08-01")
assert resp.status_code == 503
put("h1", "suite", "2026-08-02",
Recommendation(rate_cents=22000, currency="USD",
computed_at=time.monotonic(), engine_version="v9"))
fresh = client.get("/rate/h1/suite/2026-08-02")
assert fresh.status_code == 200
assert fresh.json()["engine_version"] == "v9"
Common pitfalls and edge cases
- Serving stale on miss. Never fall back to an expired entry to avoid a 404; a wrong rate is costlier than a retry.
- Wall-clock for age. Use
time.monotonic()for age math so an NTP adjustment during a long uptime cannot make a rate look fresh. - Unversioned responses. Without
engine_version, a bad rollout is unattributable; always stamp it. - No freshness in health checks. A liveness probe that ignores staleness lets a degraded node keep taking traffic.
- Blocking recompute in the request. Keep the read path pure-read; enqueue recomputation, don’t run it inline.
Related
- Price Recommendation Serving — the parent cluster covering caching and invalidation strategy.
- Rule-Based Pricing Engines — the writer whose guarded decisions this endpoint serves.
- Handling Booking.com API pagination limits — a comparable async, failure-aware integration pattern on the ingestion side.