What you get
This example builds a sales dashboard whose shell paints instantly while three slow data sources — revenue, visitors, and recent orders — fill in afterward. It all happens in a single HTTP response, with no extra client requests and no JavaScript framework.
Copy this example when a page has several independent slow queries and you want
an instant first paint instead of one spinner blocking the whole page. The
return type that does the work is [[docs/about/core-concepts/return-values|Suspense]].
The handler
The whole feature is one return type. Each awaitable in the context is deferred: the shell renders first, then each value streams back as it resolves.
from chirp import App, AppConfig, Suspense
config = AppConfig(template_dir=TEMPLATES_DIR, worker_mode="async")
app = App(config=config)
@app.route("/")
def dashboard():
"""Shell renders instantly, data blocks fill in as they resolve."""
return Suspense(
"dashboard.html",
title="Sales Dashboard", # sync — in the shell
revenue=load_revenue(), # awaitable — deferred
orders=load_orders(), # awaitable — deferred
visitors=load_visitors(), # awaitable — deferred
)
Source: examples/standalone/suspense_dashboard/app.py.
In the template, branch onis deferredto show a skeleton until the value
arrives, then render the loaded state:
{% block revenue %}
{% if revenue is deferred %}
<div class="skeleton">Loading revenue…</div>
{% else %}
<p>{{ revenue.total }} ({{ revenue.period }})</p>
{% endif %}
{% endblock %}
Each resolved block streams back as an OOB swap that replaces its skeleton in place.
Run it
PYTHONPATH=src python examples/standalone/suspense_dashboard/app.py
Open http://127.0.0.1:8000/.
Test it
pytest examples/standalone/suspense_dashboard/
Why test `is deferred`, not the value itself
A deferred key is theDEFERREDsentinel in the shell render, then resolves to
real data. Branching on bare truthiness ({% if revenue %}) treats an empty
list,0, "", and Falseas identical to the loading state — so the skeleton
can render forever and the user sees a perpetual spinner with no console error.
Always testis deferred (or membership in __chirp_defer_pending__) to
separate loading from loaded before you test the resolved value.chirp check
promotes this to a startup contract: it emits adefer_falsyWARNING when a
template self-declares a deferred key and then branches on its bare truthiness.
This example does not needdefer_blocks— every deferred block is discovered
automatically. Passdefer_blocksonly when static discovery misses a block
(for example, a deferred value handed through a macro argument).