Cache and images
Bastide runs three mechanisms that avoid doing the same work twice: the edge's HTTP cache, which serves an already produced response without calling your application; image transformation, included in the plan; and the remote build cache, compatible with Turborepo. This page explains how each one decides, how to empty it and what it costs.
The edge HTTP cache
The edge cache follows a published contract: by reading your response headers, you know without testing whether it will be stored. Nothing is stored by default; your application asks for it.
What gets stored
A response is stored when all of these hold:
- the request is a
GETor aHEAD, without aRange,AuthorizationorUpgradeheader; - the governing header carries an explicit lifetime, without
private,no-cacheorno-store; - the status is 200, 301, 302, 307, 308, 404 or 410;
- the response sets no cookie (
Set-Cookie) and does not carryVary: *; - it weighs 20 MB at most, and a 200 has a body.
The governing header is the first one present among Glowsoft-CDN-Cache-Control, CDN-Cache-Control and Cache-Control, and that one alone. Glowsoft-CDN-Cache-Control speaks only to the platform's cache; CDN-Cache-Control also to a CDN you might place in front; Cache-Control is addressed to the browser first.
The lifetime is s-maxage. max-age only counts in a targeted header (Glowsoft-CDN-Cache-Control or CDN-Cache-Control), never in Cache-Control, where it remains the browser's lifetime. It is capped at one year. Expires and heuristic lifetimes are not read.
Cache-Control: public, max-age=60, s-maxage=3600
This response is kept for one hour by the edge and one minute by the browser.
Worth knowing
- The cache key is the method, the host name, the path and the query string. Request cookies are not part of it: a storable response must not depend on them.
- A
Varyother than*stores one variant per value of the named headers. - A visitor's
Cache-Controlbypasses nothing: a browser'sno-cachedoes not force the origin. - An expired entry is never served:
stale-while-revalidateandstale-if-errorare not honoured yet. - An
If-None-Matchthat designates the stored entry receives 304, from the cache. - Redeploying the environment purges everything the previous version had stored.
Glowsoft-CDN-Cache-ControlandGlowsoft-Cache-Tagare never returned to the visitor.
Reading the cache's decision
Every response carries the Cache-Status header (RFC 9211): hit when it comes from the cache. When the cache neither read nor stored it, detail= gives the reason.
| Reason | Cause |
|---|---|
METHODE | Neither GET nor HEAD. |
PLAGE | A Range header. |
AUTORISATION | An Authorization header. |
PASSAGE | An Upgrade header (WebSocket). |
STATUT | A status outside the list. |
TEMOIN | The response sets a cookie. |
VARY-ETOILE | Vary: *. |
PRIVE | private, no-cache or no-store. |
SANS-DUREE | No explicit lifetime. |
TROP-GRAND, TROP-GRAND-EN-FLUX | Over 20 MB, announced or discovered while streaming. |
CORPS-VIDE | A 200 without a body. |
Revoking by tag
Your application can tag its responses with the Glowsoft-Cache-Tag header: comma-separated tags, 256 characters at most each. Tags are stored per project and per environment: none crosses into another tenant.
Glowsoft-Cache-Tag: product:4512, catalogue
When product 4512 changes, revoke the tag rather than the whole cache. There are two modes:
- invalidate (recommended): the stale entry is served one last time and revalidated behind the scenes, so the visitor does not see the origin slow down;
- delete: the entry is destroyed. Use it to take down content that must no longer be served.
Revocation takes effect in the region in under two seconds, according to the API.
From the console
Open “Domaines” (Domains), then the Cache de l'edge (Edge cache) card. Choose the environment (production by default), enter the tags one per line, choose the mode, then Purger (Purge).
From the command line or the API
bastide cache tag revoke --project-id prj_01JQ8ZK4M7NP2RSTVWXYZ34567 --tags '["product:4512","catalogue"]'
{
"projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
"tags": ["product:4512", "catalogue"],
"mode": "invalidate",
"idempotencyKey": "purge-product-4512-2026-10-14"
}
This body is sent as POST /v1/projects/{projectId}/cache/purge on https://api.bastide.app. Without environmentId, revocation targets production. A hundred tags at most per call, without commas. The action requires the Developer rank and a person: it is open neither to agents nor to service accounts.
Image transformations
Every site serves /_bastide/image, which resizes an image and converts it to AVIF or WebP depending on what the browser accepts. The site's own images are always allowed; a remote source must be on the project's list. Every URL is signed: an unsigned or wrongly signed URL, or one whose source is not allowed, receives 403 before anything is read.
Parameters
| Parameter | Meaning |
|---|---|
src | A path on the site (/images/a.jpg) or an https:// URL from an allowed source. |
w, h | Width and height, from 1 to 8,192 pixels. |
q | Quality, from 1 to 100; 75 by default. |
fit | cover (default), contain or fill. |
format | avif, webp or origin; otherwise negotiated from the Accept header. |
s | The signature. Any other parameter is refused. |
Signing a URL
The signature is an HMAC-SHA256 of the canonical string, encoded as base64url without padding. The canonical string takes the parameters present among src, w, h, q, fit and format, in that order, as name=value (value encoded as by encodeURIComponent), joined by &. The key is the one returned when you configure images, in hexadecimal.
import { createHmac } from "node:crypto";
const key = Buffer.from(process.env.IMAGES_SIGNING_KEY, "hex");
export function imageUrl(src, width, quality = 75) {
const p = new URLSearchParams({ src, w: String(width), q: String(quality) });
const canonical = ["src", "w", "h", "q", "fit", "format"]
.filter((n) => p.has(n))
.map((n) => `${n}=${encodeURIComponent(p.get(n))}`)
.join("&");
p.set("s", createHmac("sha256", key).update(canonical).digest("base64url"));
return `/_bastide/image?${p}`;
}
Configuring sources and the key
Open “Domaines”, then the Images card. It shows the month's transformation counter, the key generation and the list of allowed remote sources, one per line (50 at most, over https).
- Enregistrer et montrer la clé (Save and show the key) saves the list and displays the signing key. It is shown only at that moment: set it straight away as a secret variable of your application. See Environments and variables.
- Renouveler la clé (Renew the key) draws a new one: every URL signed with the old key stops being served.
- Optimiser à l'écriture (Optimise on write) gives each image uploaded to storage a lighter variant, served without transformation on read. The setting is off initially.
The same settings go through image.configure and are read with image.read, through the API (PUT and GET /v1/projects/{projectId}/images) or the command line (bastide image configure, bastide image read). Configuring requires the Maintainer rank; reading, the Reader rank. No MCP tool carries them: the key is a secret, and no tool returns a secret.
What is counted
Only a transformation actually performed is counted. An image taken from the edge cache is not, nor is an image served as is: an SVG, an animated GIF or WebP, a source under 10 KB, or a format the transformation cannot read. These images carry the X-Image-Diagnostic header, which says why.
A transformed image is kept for 31 days by the edge, within the scope of the environment and its version: redeploying expires it.
| Discovery | Standard | Pro | Sovereign | |
|---|---|---|---|---|
| Transformations included per month | 5,000 | 50,000 | 250,000 | 1,000,000 |
Beyond the included volume, the image is served as is, without transformation and without charge. An alert is sent at 80% of the month's volume: the project.images.threshold event, to your webhooks and notifications.
Limits of an image
| Limit | Value |
|---|---|
| Source dimensions | 8,192 pixels per side |
| Source weight | 25 MB |
| Output weight | 10 MB |
| Remote sources per project | 50 |
Beyond these, the response is a 422 naming the limit. An unreadable source gives 502; a transformation that does not answer, 503.
The remote build cache
Turborepo can store and read back its artefacts in a remote cache kept by Bastide, over the /v8/artifacts protocol. The Turborepo team is the project. To use it from your machine or your continuous integration:
export TURBO_API=https://api.bastide.app
export TURBO_TEAM=prj_01JQ8ZK4M7NP2RSTVWXYZ34567
export TURBO_TOKEN=<personal or service token>
Reading the cache requires the right to read the project; storing, the right to deploy (Developer rank, deploy:write scope). The platform's own builds receive their own token, limited to their project's cache and withdrawn as soon as the build ends. An artefact containing a secret is refused, and the refusal names the file. An artefact over 128 MB is not cached; Turborepo carries on without it.
Retention and purge
Retention ranges from 1 to 30 days, 7 by default; it applies to subsequent uploads. A purge takes effect on the call: a purged artefact is no longer served.
- Console: the project's “Réglages” (Settings), Cache distant Turborepo (Turborepo remote cache) card. The purge there is total.
- Agent: the
build_cache_managetool, which can also purge by Turborepo hash. - Command line:
bastide build_cache manage --project-id prj_… --retention-days 14.
{
"projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
"purge": [],
"idempotencyKey": "purge-remote-cache-2026-10-14"
}
An empty purge list empties the whole cache; a list of hashes (1,000 at most) removes only those. The response says how many artefacts and bytes were purged, and what the cache still holds.
Common errors
| Symptom or code | Cause | Remedy |
|---|---|---|
Cache-Status with detail=SANS-DUREE | No lifetime for the edge. | Add s-maxage to Cache-Control, or set Glowsoft-CDN-Cache-Control. |
detail=TEMOIN | The page sets a session cookie. | Do not set cookies on public responses. |
detail=PRIVE | private or no-store in the governing header. | Remove the directive, or target the edge with Glowsoft-CDN-Cache-Control. |
| A changed page stays old | It is still within its cache lifetime. | Revoke its tag, or redeploy. |
403 on /_bastide/image | Wrong signature, unsigned parameter, or source not on the list. | Check the canonical string and the key; add the source. |
| Images served untransformed | The month's cap is reached, or the image is exempt. | Read X-Image-Diagnostic; change tier if the cap is the cause. |
images_non_reglees | Image transformation is not in service on this installation. | Contact support. |
valeur_rendue_une_fois | Replay of a configuration call that already returned its key. | Configure again to obtain the key; it is not stored. |
See also
- Routing
- Domains
- Builds
- Storage
- Limits
- cache.tag.revoke, image.configure, build_cache.manage (operation pages, in French)