Static Assets and Cache-Breaking
Browsers cache static assets aggressively. A stylesheet served once with a
long Cache-Control lifetime may linger in the browser cache for days or
weeks, which is exactly what you want for performance – right up until you
deploy a new version of the file. At that point the browser happily keeps
serving the stale copy, and your users see broken layouts, old JavaScript,
or missing features until they perform a hard refresh.
The classic fix is cache-breaking (also called cache-busting): you change
the URL of the asset whenever its contents change, so the browser treats it
as a brand new resource and re-fetches it. Tet’s tet.static module
implements this with a per-deploy token embedded into the static view’s URL
prefix, so you can keep aggressive caching headers and still ship updates
that take effect immediately.
This page explains how the cache-breaker works, how to wire it into your application, and how to emit cache-broken URLs from your templates.
How the cachebreaker works
When you include tet.static, a single cache-breaker token is
generated once, at configuration time, and stored on the Pyramid registry as
config.registry.cachebreaker. The token is a timestamp – the current
time in milliseconds since the epoch, zero-padded to twelve digits:
config.registry.cachebreaker = f"{int(time.time() * 1000):012d}"
Because the token is computed once per process startup, it is effectively a
per-deploy value: every fresh deployment (which restarts the
application) gets a new token, and every URL generated during that deploy
shares the same token. A typical token looks like 001718539201234.
The token is woven into the URL prefix of your static view rather than
appended as a query string. When you register a static view with
add_static_view_with_breaker(), the literal {breaker} placeholder
in the view name is replaced by the current token. So a view registered as
static/{breaker} actually serves assets from
static/001718539201234/.... Generating a URL for an asset with Pyramid’s
standard static_url() then naturally produces
a URL that contains the token, with no extra work in your view code or
templates.
Handling stale and premature URLs
Embedding the token in the path raises an obvious question: what happens to
URLs that were generated by a previous deploy and are still cached in a
user’s browser or referenced by an already-loaded page? tet.static
installs a small redirector route alongside the static view to handle these
cases gracefully:
Older token (the request’s breaker is less than the current one): the asset has been re-deployed, so the redirector issues a
301 Moved Permanentlyto the same asset path under the current token. The browser follows the redirect and picks up the fresh asset.Newer token (the request’s breaker is greater than the current one): this happens transiently during a rolling deploy, when a page served by a newer instance points at assets that an older instance has not caught up with yet. The redirector returns
503 Service Unavailablewith aRetry-After: 3header so the client retries shortly.Matching token but no such asset: a plain
404 Not Found.
You do not have to configure any of this – registering the view with
add_static_view_with_breaker() wires up the redirector automatically.
Wiring it into your application
Pull the module in with config.include and then register your static
views using the add_static_view_with_breaker() directive. The
include call adds two directives to the Configurator:
set_cachebreaker() and add_static_view_with_breaker().
from tet.config import application_factory
@application_factory()
def main(config):
config.include("tet.static")
config.add_static_view_with_breaker(
name="static/{breaker}",
path="myapp:static",
)
config.scan()
The name argument must contain the literal {breaker}
placeholder; if it is missing, add_static_view_with_breaker() raises a
ValueError. The path argument is an ordinary Pyramid asset
specification pointing at the directory that holds your static files. Any
extra keyword arguments are forwarded to the underlying
add_static_view(), so you can pass
options such as cache_max_age:
config.add_static_view_with_breaker(
name="static/{breaker}",
path="myapp:static",
cache_max_age=31536000, # one year -- safe, because the URL changes per deploy
)
Because each deploy mints a new token and therefore a new URL, you can set a
very long cache_max_age without ever risking a stale asset. That is the
whole point of cache-breaking: maximum caching, zero staleness.
Overriding the token
The default timestamp token is convenient, but sometimes you want a
deterministic value – for example, to tie the token to a release tag, a Git
commit hash, or a value injected by your CI pipeline so that several
processes in a cluster all agree on the same token. Use the
set_cachebreaker() directive before registering your static
views:
@application_factory()
def main(config):
config.include("tet.static")
config.set_cachebreaker(config.registry.settings["myapp.release"])
config.add_static_view_with_breaker(
name="static/{breaker}",
path="myapp:static",
)
config.scan()
set_cachebreaker() simply replaces config.registry.cachebreaker
with the value you supply. Since add_static_view_with_breaker() reads
the registry at the moment it builds the URL, the override only takes effect
for views registered afterward – so always set it first.
Using static_url in templates
Once the cache-broken static view is registered, you do not need to think
about the token in day-to-day template work. Generate asset URLs the normal
Pyramid way, with request.static_url(), passing the same asset
specification style you used for path. The generated URL automatically
includes the current cachebreaker token because it is baked into the view’s
URL prefix.
In a Tonnikala template, use $ interpolation – the expression resolves
the chain of attribute access and the method call as a single unit:
<link href="$request.static_url('myapp:static/style.css')" rel="stylesheet">
<script src="$request.static_url('myapp:static/app.js')"></script>
<img src="$request.static_url('myapp:static/img/logo.png')" alt="Logo">
At render time these expand to URLs such as:
/static/001718539201234/style.css
/static/001718539201234/app.js
/static/001718539201234/img/logo.png
After the next deploy the token changes, every emitted URL changes with it,
and browsers fetch the new assets – while any page still pointing at the old
token gets a 301 redirect to the current one.
You can of course use the same call from Python view code or any other renderer:
@view_config(renderer="myapp:templates/index.tk")
def index(request):
return {
"stylesheet_url": request.static_url("myapp:static/style.css"),
}
Note
Always go through request.static_url() (or its template
equivalent) rather than hard-coding the /static/... path yourself.
Hard-coded paths will not contain the cachebreaker token, defeating the
whole mechanism, and will break if the token-bearing prefix ever changes.
See also
Templating with Tonnikala – writing Tonnikala templates and using
$interpolation, including expressions like$request.static_url(...).Configuration – the application factory,
config.includeand registering directives during configuration.