Key takeaways
- A Smart CDN URL is a workspace, a Template name, and a file path, with query parameters feeding the Template.
- The Template reads those parameters as Assembly Variables, so ${fields.w} can become the width of the resize.
- Only the first request for a given parameter combination is encoded; the rest are answered from cache.
Serving responsive images is mostly a decision about where the list of variants lives. Generating every size ahead of time ties the storage bill to the number of breakpoints, while deriving sizes on request moves the cost to the first request for each variant and puts a cache in front of everything after it.
What matters most
- AVIF and WebP can be selected from the browser Accept header through the ${browser.wanted_image_format} variable.
- Restrict the widths a URL may ask for, because every distinct combination is a separate encode.
- A signed URL expires, and the time left on that signature also bounds how long the result stays cacheable.
Put the Template behind the URL
A Smart CDN URL is made of three parts and a query string: the Workspace as a subdomain of tlcdn.com, the name of a Template, and the path of the file to work on. Requesting it runs that Template, and the query parameters arrive inside it as Assembly Variables, so a Template that resizes to ${fields.w} turns ?w=640 into a 640-pixel-wide result. Nothing about the variant list is compiled into the front end.
That split decides what belongs where. The Template holds the pipeline — import the original, transform it, serve it — while the URL carries only the few values that legitimately vary per request. Keeping the pipeline server-side means a change to encoding quality or a new processing Step ships without touching a single page of markup.
{
"steps": {
"imported": {
"robot": "/s3/import",
"credentials": "my_s3_credentials",
"path": "/images/${fields.input}"
},
"resized": {
"use": "imported",
"robot": "/image/resize",
"resize_strategy": "fit",
"width": "${fields.w}"
},
"served": {
"use": "resized",
"robot": "/file/serve",
"cache_duration": 604800
}
}
}Workspace and Template
The subdomain identifies the Workspace and the first path segment names the Template to run.
File path
The rest of the path identifies which original the Template should operate on.
Query parameters
These reach the Template as Assembly Variables, which is how one URL serves many sizes.
Decide which parameters a URL may set
Every distinct combination of parameters is a distinct result, which means a distinct cache entry and, the first time it is asked for, a distinct encode. A layout that requests whatever width the container happens to be will generate an unbounded set of near-identical images, each paid for once and then rarely reused. A short list of widths chosen from real breakpoints costs a handful of encodes and then hits cache for everything after.
This is also the abuse surface. A URL that accepts arbitrary dimensions lets anyone who finds it enumerate widths and turn your delivery endpoint into an encoding bill. Signing the URL removes that possibility entirely, and validating inside the Template — clamping a requested width, or falling back to a default — contains the damage where signing is not practical.
A fixed set of widths
Pick them from the breakpoints the design actually uses rather than from container measurements.
Cache fragmentation
Near-identical variants split the traffic that would otherwise share one cached result.
Clamp in the Template
Bound the values a request can ask for so an unexpected parameter cannot become an expensive one.
Sign the URL, and treat the expiry as a cache setting
Signing happens on the back end, because it needs the Auth Secret. The string to sign is the Workspace, Template name, and file path followed by the query parameters sorted in descending order of their keys; the HMAC is SHA256, and the result goes into a sig parameter prefixed with sha256:. Note that Smart CDN signatures use SHA256 while regular API request signatures use SHA384, and that the Auth Key must be enabled for Smart CDN use.
The exp parameter, a millisecond UNIX timestamp, is where the interesting trade-off lives. It is obviously an access control: after that moment the URL stops working. It is also a cache control, because the effective cache lifetime of a signed response is bounded by the time left on its signature. A short expiry tightens security and throws away cache reuse; a long one keeps results warm and leaves the URL usable for longer.
import { Transloadit } from 'transloadit'
const transloadit = new Transloadit({
authKey: process.env.TL_KEY,
authSecret: process.env.TL_SECRET,
})
const url = transloadit.getSignedSmartCDNUrl({
workspace: 'my-workspace',
template: 'responsive-image',
input: 'canoe.jpg',
urlParams: { w: 640 },
})Descending key order
Parameters are sorted by unicode code point in descending order before the string is signed.
sig and exp
Modern URLs use these two; the legacy s and expires parameters are deprecated.
Expiry bounds cache
Choose the window from content sensitivity and from how much cache reuse you want.
Let the Accept header pick the format
Modern formats are worth real bytes, and the browser already announces which ones it will take. The ${browser.wanted_image_format} Assembly Variable resolves that announcement to avif, webp, or jpg, honouring the quality weights in the header, so a Template that reads it into the format parameter of /image/resize hands every browser the best format it explicitly claims to support. None of that selection logic ends up in your markup, and no second URL is needed to express it.
The subtlety is what jpg actually means. A wildcard media range does not opt a client in, by design, so curl, most SDKs, and server-to-server callers all land on that value — it describes what the caller will accept, not what the file is. Feeding it straight into format converts a transparent PNG or an animated GIF to JPEG for every one of those requests. Mapping the fallback to null instead leaves /image/resize on the input format, so browsers get a modern format and everything else gets the original untouched.
{
"steps": {
"resized": {
"use": ":original",
"robot": "/image/resize",
"width": 800,
"format": "${browser.wanted_image_format === 'jpg' ? null : browser.wanted_image_format}"
},
"served": {
"use": "resized",
"robot": "/file/serve"
}
}
}avif and webp
Selected when the browser explicitly accepts them, with quality weights taken into account.
The jpg fallback
Returned for missing or wildcard-only headers, which covers most non-browser callers.
Map the fallback to null
Keeps the resize on the input format so transparency and animation survive the round trip.
Cache hit rate is the whole economic argument
Two Robots split the bill and understanding which is which explains most surprises. /file/serve charges only when the CDN has no cached copy and asks for the content to be produced again, so encoding cost tracks the miss rate rather than the traffic. /tlcdn/deliver handles global distribution, is implied by using the tlcdn.com domain rather than written into the Assembly Instructions, and bills the bandwidth with a minimum charge of 102,400 bytes per delivery.
The defaults tell browsers to cache for 72 hours and CDNs for 24, and cache_duration on /file/serve overrides both at once. Set it against how long the underlying original stays valid: content that never changes can be cached for a long time, while files deleted after a day should not outlive themselves at the edge. The one arrangement to avoid is pointing markup straight at a serve endpoint with no CDN in front, where a popular page turns every view into a fresh encode.
Charged on regeneration
A cached variant costs delivery only; the encode is paid for when the cache misses.
cache_duration
Sets the browser and CDN cache windows together, replacing the 72-hour and 24-hour defaults.
Always front it with a CDN
Serving directly from an origin endpoint turns each page view into another encode.
Vary: Accept
Negotiated formats are cached separately, so each format multiplies the entries a width produces.
Keep the originals somewhere you control
The Template starts by importing the original, typically with /s3/import from your own bucket, using a path built from the URL such as /images/${fields.input}. Derivatives are a cache, not a record. Keeping the originals in storage you own means the relationship stays one-directional: everything served can be reconstructed from something you hold, and nothing important exists only as a cached variant.
That arrangement is what makes a redesign cheap. New breakpoints mean new parameter values and a warm-up period while the cache fills, not a migration job over stored files. It is also the reason a format change is unremarkable: the Template asks for something different, old variants age out, and the originals never moved.
Import per request
The Template pulls the original from your storage using a path taken from the URL.
Derivatives are disposable
Anything the CDN holds can be produced again, so losing a cached variant costs one encode.
Breakpoints can change
A new size is a new parameter value rather than a batch job over stored files.
Technical details worth knowing
- A Smart CDN URL takes the form of a workspace subdomain on tlcdn.com followed by the Template name and the file path, and its query parameters arrive inside the Template as Assembly Variables such as ${fields.w}.
- The served response tells browsers to cache for 72 hours and CDNs for 24 hours by default, and the cache_duration parameter on /file/serve overrides both of those at once.
- /file/serve is charged only when the CDN holds no cached copy and asks for the content to be produced again, which ties encoding cost to the cache hit rate rather than to traffic.
- Global delivery is handled by /tlcdn/deliver, which is implied by the tlcdn.com domain rather than written into Assembly Instructions, and which bills bandwidth with a minimum charge of 102,400 bytes.
- Signing is an HMAC SHA256 over the workspace, Template name, file path, and the query parameters sorted in descending order of their keys, submitted as a sig parameter prefixed with sha256 and a colon.
- The exp parameter is a millisecond UNIX timestamp, and because the effective cache lifetime of a signed response is bounded by the time remaining on the signature, the expiry is a caching decision as much as a security one.
- The ${browser.wanted_image_format} Assembly Variable resolves to avif when AVIF is explicitly accepted, to webp when WebP is the best explicitly accepted modern format, and to jpg when the Accept header is absent or carries only wildcards, with quality weights honoured.
- A wildcard media range deliberately does not opt a client into a modern format, which keeps curl, SDK, and server-to-server callers on the jpg value, and mapping that value to null instead leaves /image/resize on the input format.
- Smart CDN responses carry a Vary: Accept header, so caches hold a separate entry per negotiated format and the number of formats in play multiplies the number of cached variants.
A practical approach
- 1
Keep the originals in storage you control and import them into the Template with /s3/import.
- 2
Expose a short, fixed list of widths in the URL rather than an unbounded parameter.
- 3
Sign URLs on the back end with an Auth Key that is enabled for Smart CDN use.
- 4
Read ${browser.wanted_image_format} in the Template, mapping the jpg fallback to null.
When Transloadit is useful
Use a Template that imports the original from your own storage, transforms it with /image/resize, and ends in /file/serve. Request it through a tlcdn.com URL whose query parameters supply the Assembly Variables that the Template reads.
Architecture boundary
Format negotiation is offered to the Template rather than applied for you, so a Template that never reads ${browser.wanted_image_format} keeps serving exactly what it served before. The variable also resolves to jpg for missing or wildcard-only Accept headers, which is a statement about the client rather than about the file, so feeding that fallback straight into a format parameter flattens transparency and animation for every non-browser request.
Frequently asked questions
Does Smart CDN serve WebP or AVIF automatically?
It will, but only once the Template asks. Read ${browser.wanted_image_format} into the format parameter and each browser receives the best format it explicitly accepts. A Template that never reads the variable keeps serving whatever it served before.
Why did my transparent PNG come back as a JPEG?
Because the caller sent no Accept header, or only a wildcard one, and the Template used that jpg fallback as a literal format. The fallback describes client support rather than the file, so map it to null and let the resize keep the input format instead.
How many widths should I expose?
As few as the design genuinely needs, usually a handful taken from real breakpoints. Each additional combination is another cache entry and another first-request encode, and variants close in size rarely earn the traffic they take from one another.
Why is my encoding cost higher than the number of images suggests?
Almost always cache misses. Unbounded width parameters, short signature expiries, and a low cache_duration all shorten the life of a cached result, and every expiry means the next request pays for the encode again.
Do I have to sign every URL?
It is not mandatory, but an unsigned URL that accepts arbitrary parameters can be enumerated by anyone who finds it, and each new combination is an encode you pay for. Sign the URLs, or clamp the accepted values inside the Template.
Where do the original files live?
In your own storage. The Template imports the original per request, commonly with /s3/import using a path built from the URL, which keeps the CDN holding derivatives that can always be produced again from something you control.