Tutorial: Production Deployment Checklist
On this page
A checklist-style walkthrough for taking a router-based application to production, based on the configuration surface and behaviors documented across this site.
1. Choose an execution mode deliberately
ExecutionMode.SINGLE(default) — lowest latency, no fallback if the top route's handler fails.ExecutionMode.SEQUENTIAL— tries the next-best candidate on failure; good default for production if you have overlapping/fallback-capable routes.ExecutionMode.PARALLEL— highest cost (executes multiple candidates concurrently), useful when you want to race multiple strategies and merge/pick the best.
See Configuration → ExecutionConfig and note the sync/async behavioral differences documented in ExecutionEngine (async paths do not retry; async parallel discards all results on timeout rather than keeping partial results).
2. Set explicit confidence thresholds for your domain
The defaults (high=0.85, medium=0.60, fallback=0.40) are reasonable starting points but should be tuned against your actual route examples and query distribution. Log result.trace.candidates scores during a staging period and adjust thresholds based on observed separation between correct and incorrect top matches.
3. Always set a fallback handler
router.set_fallback(ClarifyOrEscalateHandler())
Without one, _handle_no_match either raises LookupError (if raise_on_no_match=True) or returns a failed RoutingResult — neither is a good default end-user experience. See HierarchicalRouter._handle_no_match.
4. Wire up observability
from zerda.router import ObservabilityConfig, LogLevel
config = RouterConfig(
observability=ObservabilityConfig(
log_level=LogLevel.INFO,
structured_logging=True, # JSON logs — pipe into your log aggregator
slow_route_ms=800.0, # tune to your latency SLO
metrics_window=2000,
),
)
Expose router.metrics() and router.cache_stats() on a health/metrics endpoint (e.g. a /internal/router-metrics route in your web framework) so you can monitor match rate, latency percentiles, and cache hit rate over time — see MetricsCollector.snapshot.
5. Decide on caching TTL deliberately
CacheConfig.ttl_seconds (default 300) controls how long a route decision is trusted. Since most route/group mutation methods already clear the cache, TTL mainly guards against stale decisions after score_bias drifts significantly via feedback. If your feedback loop adapts quickly (low min_samples_to_adapt, high alpha), consider a shorter TTL.
6. Persist feedback state across deploys
config.feedback.persist_path = "/var/lib/myapp/router_feedback.json"
Use HierarchicalRouter as a context manager (or call router._feedback.save() on shutdown) so learned biases survive restarts/redeploys rather than resetting to neutral every time.
7. Guard the embedding model load
HierarchicalRouter() construction loads the sentence-transformer model synchronously and can raise ImportError if sentence-transformers isn't installed. In containerized deployments, bake the model into the image (or pre-warm a shared cache volume) to avoid a slow, network-dependent cold start on every container boot.
8. Decide your LLM fallback posture
If using RouterConfig.llm_model, remember LLMScorer fails silently (returns empty scores) on any error, including missing openai, bad credentials, or rate limiting — there's no built-in alerting for LLM fallback failures. Add your own monitoring around this if the LLM tier is load-bearing for your accuracy targets.
9. Test the no-match and disabled-route paths explicitly
# Confirm your fallback handler behaves correctly:
result = router.route_query("completely unrelated gibberish query xyz123")
assert not result.matched
# Confirm disabling a route doesn't break routing:
router.get_route("some_route").disable()
result = router.route_query("a query that would normally hit some_route")
Related pages
- Configuration — full field reference
- Architecture — request lifecycle and diagrams
- Advanced Usage — streaming, chaining, feedback tuning