Entender e otimizar preços de redes de entrega de conteúdo
Uma taxa maior de acertos do cache pode reduzir sua cobrança na origem, mantendo as cobranças de requisições e entrega da CDN quase inalteradas. Para otimizar os custos da CDN, primeiro separe essas métricas de cobrança. Depois, verifique se sua política de cache e suas URLs de imagens servem os bytes corretos antes de aumentar seu prazo de validade.

O que é uma CDN?
Uma CDN recebe requisições perto de seus usuários e serve uma resposta em cache ou a busca na origem. Um acerto do cache evita parte do trabalho na origem, mas o nó de borda ainda processa a requisição e entrega bytes ao usuário. O cache do navegador pode evitar totalmente essa requisição pela rede; o cache de borda não pode.
Visão geral dos modelos de preços de CDN
Os modelos de preços podem se sobrepor: um plano com cobrança por uso pode cobrar tanto por requisições quanto por transferência, com tarifas separadas para computação e segurança. Comece pela sua carga de trabalho e aplique as franquias, faixas, regiões e compromissos mínimos do plano escolhido.
| Modelo | O que verificar antes de estimar |
|---|---|
| Plano mensal com preço fixo | Franquias incluídas de requisições e transferência, tráfego elegível e o que acontece ao excedê-las |
| Entrega com cobrança por uso | Bytes entregues por região de cobrança, contagens de requisições, franquias gratuitas e limites das faixas |
| Contrato com compromisso de gasto | Gasto mínimo, produtos incluídos, excedentes e se o compromisso não utilizado expira |
Na data de 8 de outubro de 2026, os preços do Amazon CloudFront incluem planos com preço fixo sem cobrança de excedentes, além de preços por uso. Os planos com preço fixo ainda têm franquias de uso; leia as condições dos planos antes de considerá-los uma oferta de entrega ilimitada. Consulte os planos da Cloudflare, os preços da Fastly e os preços da Akamai para conhecer pacotes ou obter cotações. Se o link de preços da Akamai estiver indisponível, sua página de Download Delivery tem um contato de vendas. O menor preço anunciado por um provedor não é uma estimativa para sua carga de trabalho.
Principais fatores que influenciam os custos de CDN
Mantenha quatro linhas na sua planilha, mesmo quando um plano inclui algumas delas em um pacote:
| Categoria de custo | Dados da carga de trabalho | Como estimar |
|---|---|---|
| Requisições | Requisições mensais dos usuários por categoria faturável | Divida as requisições faturáveis pela unidade da tarifa e multiplique o resultado por essa tarifa |
| Transferência | Bytes entregues aos usuários por região de cobrança | Converta bytes para a unidade de GB do provedor e aplique as franquias e faixas |
| Origem | Buscas na origem, bytes transferidos e trabalho da aplicação | Aplique separadamente as tarifas de armazenamento, requisições, transferência de saída e computação da origem |
| Computação e extras | Invocações de funções, duração ou unidades de CPU, transformações, logs e segurança | Use a métrica de cobrança de cada produto habilitado; desconte apenas a franquia aplicável a ele |
Largura de banda e transferência de dados
Um mês hipotético com 10 milhões de requisições de 200 KB em média entrega 2.000 GB, usando KB e GB decimais. Com tarifas fictícias de $0,08/GB e $1/milhão de requisições, isso equivale a $160 de transferência mais $10 de requisições, antes de franquias, custos da origem, computação, impostos ou compromissos de gasto. São exemplos de cálculo, não cotações de provedores. Use os bytes de transferência efetivamente faturáveis em vez dos tamanhos dos arquivos sem compressão e verifique se o provedor define GB como decimal ou binário.
Se 80% dessas requisições resultarem em acertos do cache de borda, um modelo simples com uma busca na origem por ausência no cache deixa 2 milhões de buscas na origem. Isso não reduz os 10 milhões de requisições dos usuários nem os 2.000 GB entregues pela CDN. Revalidação, agrupamento de requisições, requisições de intervalos de bytes e tamanhos desiguais dos objetos podem alterar a estimativa da origem; confira essa estimativa com os logs da origem em vez de multiplicar todos os bytes pela taxa de acertos por requisição.
Distribuição geográfica
Agrupe a entrega pelas regiões de cobrança do provedor, não pela localização da origem. Tanto os preços por uso do CloudFront quanto os preços da Fastly têm tabelas regionais de transferência. Uma tarifa média ponderada por região depende da distribuição do seu tráfego. Mover a origem, por si só, não altera a métrica de cobrança da entrega dos nós de borda aos usuários.
Recursos e segurança
Liste os produtos habilitados ao lado da estimativa de entrega. Regras de WAF, geração de logs, otimização de imagens e TLS podem estar incluídos em um pacote, ter cobrança por uso ou ser contratados separadamente. Verifique também as cobranças na origem: atualmente, o CloudFront isenta a transferência de origens AWS para o CloudFront, mas isso não isenta todas as cobranças de armazenamento ou requisições da aplicação.
Computação de borda e recursos modernos
Gerar antecipadamente variantes populares de imagens transfere o trabalho de codificação para uma compilação ou tarefa de processamento. Isso acrescenta custos de armazenamento e compilação; portanto, meça esses custos junto com a economia na entrega. A computação de borda tem métricas de cobrança específicas de cada produto: a Fastly lista requisições e milissegundos de vCPU, enquanto outros produtos usam invocações ou duração, e os planos podem incluir computação. Use a métrica de cobrança do produto escolhido.
O laboratório local a seguir verifica a seleção de URLs, os cabeçalhos de cache da origem, a
compressão e o monitoramento. Ele não implanta uma CDN nem mede a taxa de acertos do cache de um
provedor. Use Bash,
Node.js 24.15.0 ou uma versão posterior da linha Node 24 ainda
mantida, ou Node 26.5.0 ou posterior da linha Node 26, além do
Corepack instalado separadamente e do cURL. O exemplo usa remoção
nativa de tipos do TypeScript e arquivos .mts para manter explícito o
comportamento dos módulos. As versões do Yarn e das dependências são fixadas abaixo; Node 24.15.0,
26.5.0 e 26.8.1 são as versões testadas, não requisitos mínimos intrínsecos das APIs. O Sharp gera
imagens sintéticas, não um fluxo de processamento de imagens para produção.
Cole esta configuração em um diretório onde cdn-cost-lab não exista. O subshell
preserva o diretório e as opções do shell chamador, e os comandos param em caso de falha:
(
for tool in node corepack curl mkdir; do
command -v "$tool" >/dev/null || { printf 'Missing prerequisite: %s\n' "$tool" >&2; exit 1; }
done
node -e 'const [major, minor] = process.versions.node.split(".").map(Number); if (!(major === 24 && minor >= 15 || major === 26 && minor >= 5)) { console.error("Use Node 24.15+ or 26.5+ within those majors"); process.exit(1) }' &&
mkdir cdn-cost-lab && cd cdn-cost-lab &&
printf '%s\n' '{"private":true,"type":"module","packageManager":"yarn@4.12.0","dependencies":{"compression":"1.8.2","express":"5.2.1","negotiator":"1.1.0","sharp":"0.35.3"}}' > package.json &&
printf '%s\n' 'nodeLinker: node-modules' 'enableGlobalCache: false' > .yarnrc.yml &&
printf '' > yarn.lock &&
corepack yarn install
)
Se a instalação falhar após criar o diretório, mantenha-o e tente novamente com o bloco abaixo.
Não execute novamente a criação do projeto nesse diretório. Salve os arquivos a seguir dentro de
cdn-cost-lab.
(cd cdn-cost-lab && corepack yarn install)
Salve este código como prepare-demo.mts. Ele cria um PNG verde com transparência e
um JPEG vermelho opaco com o mesmo nome-base, além de suas duas larguras em cada formato declarado.
Manter a extensão de origem no diretório de variantes impede a colisão entre as duas identidades.
O script recusa um diretório dist existente; uma compilação com falha pode
deixar resultados parciais, então use um laboratório novo para outra compilação.
import { createHash } from 'node:crypto'
import { mkdir, writeFile } from 'node:fs/promises'
import sharp from 'sharp'
await mkdir('dist')
await mkdir('dist/assets', { recursive: true })
const script = 'console.log("cdn demo")\n'.repeat(1024)
const hash = createHash('sha256').update(script).digest('hex').slice(0, 8)
await writeFile(`dist/assets/app.${hash}.js`, script)
await writeFile('dist/assets/app.js', script)
for (const [source, formats, background] of [
['logo.png', ['png', 'avif', 'webp'], { r: 0, g: 180, b: 0, alpha: 0.5 }],
['logo.jpg', ['jpeg', 'avif', 'webp'], { r: 200, g: 0, b: 0, alpha: 1 }],
]) {
for (const width of [320, 1024]) {
const directory = `dist/images/w/${width}/${source}`
await mkdir(directory, { recursive: true })
for (const format of formats) {
const extension = format === 'jpeg' ? 'jpg' : format
await sharp({ create: { width, height: width / 2, channels: 4, background } })
.toFormat(format)
.toFile(`${directory}/image.${extension}`)
}
}
}
console.log(`/assets/app.${hash}.js`)
Salve este código como variants.mts. Apenas as duas fontes públicas declaradas têm
variantes. Um parâmetro de consulta desconhecido retorna null: ele pode
selecionar uma versão diferente, um recorte ou um objeto assinado. Apenas
w e o parâmetro utm_source, conhecido como inócuo
neste laboratório, podem ser removidos. Amplie essa lista somente após verificar o contrato da
origem. Larguras desconhecidas usam 1024; parâmetros de largura duplicados são recusados.
import Negotiator from 'negotiator'
const WIDTHS = new Map([['small', '320'], ['large', '1024']])
const PUBLISHED = new Map([
['/images/logo.png', new Map([['image/png', 'png'], ['image/avif', 'avif'], ['image/webp', 'webp']])],
['/images/logo.jpg', new Map([['image/jpeg', 'jpg'], ['image/avif', 'avif'], ['image/webp', 'webp']])],
])
export function buildVariantUrl(requestUrl, accept) {
const url = new URL(requestUrl)
const formats = PUBLISHED.get(url.pathname)
if (!formats) return null
for (const key of url.searchParams.keys()) {
if (key !== 'w' && key !== 'utm_source') return null
}
if (url.searchParams.getAll('w').length > 1) return null
// The source format wins wildcard ties; q=0 excludes a format.
const accepted = new Negotiator({ headers: { accept: accept ?? '*/*' } })
.mediaType([...formats.keys()])
const format = formats.get(accepted)
if (format === undefined) return null
const width = WIDTHS.get(url.searchParams.get('w')) ?? WIDTHS.get('large')
const name = url.pathname.slice('/images/'.length)
url.pathname = `/images/w/${width}/${name}/image.${format}`
url.search = ''
url.hash = ''
return url.toString()
}
Isso seleciona uma URL, não uma operação de codificação. Seu modelo ou código chamador deve
requisitar a URL retornada. Um resultado null significa que esta função
auxiliar não tem uma variante segura; isso não é permissão para enviar um formato rejeitado.
Uma alternativa em PNG mantém a transparência. Em produção, publique apenas variantes que existam
e tenham sido verificadas, e versione suas URLs sempre que seus bytes mudarem.
Servir formatos diferentes a partir de uma única URL exige uma configuração de cache compatível.
O recurso Vary for Images da Cloudflare
exige uma regra de variantes configurada e está disponível nos planos Pro, Business e Enterprise,
mas não no Free. Vary: Accept sozinho não o habilita. URLs com formato explícito
separam as representações, mas sua CDN ainda precisa permitir o armazenamento dessas respostas
em cache.
Implementar um cache eficaz
Um prazo de validade de um ano para immutable só é seguro quando a URL muda
sempre que os bytes mudam. O nome do arquivo JavaScript do laboratório contém um hash do conteúdo.
As imagens sem versão ficam atualizadas por uma hora e podem ser servidas desatualizadas por até
um dia durante a revalidação, então não há garantia de que uma substituição chegue aos clientes em
uma hora. Nunca sobrescreva uma URL com hash de conteúdo com bytes diferentes.
Decida se uma resposta pode ser armazenada em cache pela rota, não pela extensão.
/account/avatar.jpg e /api/private.css podem conter dados específicos de
cada usuário. Salve este código como server.mts; apenas os pontos de montagem
públicos substituem a política padrão no-store. As rotas de conta são
demonstrações sem autenticação, não uma implementação de autenticação. O laboratório escuta apenas
na interface de loopback.
import express from 'express'
const FINGERPRINTED = /\.[0-9a-f]{8,}\.(?:css|js|jpe?g|png|gif|webp|avif|woff2?)$/
function cacheControlFor(filePath) {
return FINGERPRINTED.test(filePath)
? 'public, max-age=31536000, immutable'
: 'public, max-age=3600, stale-while-revalidate=86400'
}
const app = express()
app.use((_req, res, next) => {
res.setHeader('Cache-Control', 'no-store')
next()
})
const publicFiles = {
setHeaders: (res, filePath) => res.setHeader('Cache-Control', cacheControlFor(filePath)),
}
app.use('/assets', express.static('dist/assets', publicFiles))
app.use('/images', express.static('dist/images', publicFiles))
app.get('/account/avatar.jpg', (_req, res) => res.type('jpg').send('per-user bytes'))
app.get('/api/private.css', (_req, res) => res.type('css').send(':root{--brand:rebeccapurple}'))
app.get('/api/failure.css', (_req, _res, next) => next(new Error('Demo failure')))
app.use((_req, res) => res.status(404).send('Not found'))
app.use((_error, _req, res, _next) => {
res.setHeader('Cache-Control', 'no-store')
res.status(500).send('Request failed')
})
const port = Number(process.env.PORT ?? 3000)
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error('Invalid PORT')
const server = app.listen(port, '127.0.0.1', (error) => {
if (error) {
console.error(`Listen failed: ${error.code}`)
process.exitCode = 1
return
}
console.log(`http://127.0.0.1:${server.address().port}`)
})
process.once('SIGINT', () => server.close())
process.once('SIGTERM', () => server.close())
Dentro de cdn-cost-lab, gere os arquivos de teste e inicie a origem.
PORT=0 escolhe uma porta não utilizada; copie a URL da origem impressa para
usar no próximo terminal. Pare o servidor em primeiro plano com Ctrl+C.
node prepare-demo.mts && PORT=0 node server.mts
Em um segundo terminal dentro do laboratório, defina DEMO_ORIGIN com essa URL
impressa. Execute este código complementar para selecionar e buscar uma variante WebP pequena;
a fonte PNG e a largura permanecem no caminho dela:
DEMO_ORIGIN='http://127.0.0.1:REPLACE_WITH_PRINTED_PORT'
node --input-type=module - "$DEMO_ORIGIN" <<'NODE'
import { buildVariantUrl } from './variants.mts'
const url = buildVariantUrl(`${process.argv[2]}/images/logo.png?w=small`, 'image/webp')
if (url === null) throw new Error('No acceptable variant')
const response = await fetch(url)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const bytes = new Uint8Array(await response.arrayBuffer())
console.log(new URL(url).pathname, response.headers.get('content-type'), bytes.length)
NODE
Espere /images/w/320/logo.png/image.webp, image/webp e uma contagem positiva de
bytes. Isso comprova que o objeto selecionado está acessível; inspecione seus pixels ao substituir
esta demonstração pela sua própria compilação. Requisite o caminho do recurso com hash impresso
pela compilação para inspecionar immutable. Estes comandos mostram as políticas
para objetos sem versão, privados e ausentes; use o mesmo DEMO_ORIGIN:
curl -fsSI "$DEMO_ORIGIN/assets/app.js" &&
curl -fsSI "$DEMO_ORIGIN/account/avatar.jpg" &&
curl -sSI "$DEMO_ORIGIN/assets/absent.js"
A primeira resposta tem max-age=3600, stale-while-revalidate=86400; a segunda e a resposta 404 têm
no-store. Esses cabeçalhos da origem não comprovam que uma CDN implantada os
respeita. Verifique suas regras de cache, cookies, tratamento de autorização e logs de status do
cache antes de habilitar o cache compartilhado.
Monitoramento e otimização de desempenho
Uma sondagem útil deve rejeitar respostas fora da faixa 2xx, ler o corpo inteiro e limitar a
duração da requisição completa. Salve este código como edge-monitor.mts. Ele espera
cinco minutos entre amostras sequenciais e cancela uma busca ativa ou um temporizador de espera ao
pressionar Ctrl+C. Seu tempo limite de cinco segundos inclui a transferência do corpo, então um
recurso grande em uma conexão lenta pode falhar mesmo que a transferência esteja progredindo.
import { setTimeout as sleep } from 'node:timers/promises'
async function probeEdge(url, timeoutMs = 5000, signal) {
const start = performance.now()
const deadline = AbortSignal.timeout(timeoutMs)
const response = await fetch(url, {
signal: signal ? AbortSignal.any([signal, deadline]) : deadline,
})
try {
if (!response.ok) return { url, ok: false, status: response.status }
let bytes = 0
if (response.body) {
for await (const chunk of response.body) bytes += chunk.byteLength
}
return {
url, ok: true, status: response.status, bytes,
duration: performance.now() - start,
cache: response.headers.get('cf-cache-status') ?? 'unknown',
}
} finally {
if (!response.bodyUsed) await response.body?.cancel()
}
}
function formatProbe(result) {
const { origin, pathname } = new URL(result.url)
if (!result.ok) return `${origin}${pathname} FAIL ${result.error ?? `HTTP ${result.status}`}`
return `${origin}${pathname} ${result.status} ${result.duration.toFixed(0)}ms ${result.bytes}B cache:${result.cache}`
}
async function monitorEdge(url, signal, intervalMs = 300_000) {
while (!signal.aborted) {
const result = await probeEdge(url, 5000, signal)
.catch((error) => ({ url, ok: false, error: error.name }))
if (signal.aborted) break
console.log(formatProbe(result))
await sleep(intervalMs, undefined, { signal }).catch((error) => {
if (error.name !== 'AbortError') throw error
})
}
}
const url = process.argv[2]
if (!url) throw new Error('Usage: node edge-monitor.mts URL')
const controller = new AbortController()
process.once('SIGINT', () => controller.abort())
process.once('SIGTERM', () => controller.abort())
await monitorEdge(url, controller.signal)
Execute node edge-monitor.mts "$DEMO_ORIGIN/assets/app.js". Uma linha que indica sucesso inclui HTTP 200, milissegundos
decorridos, bytes e cache:unknown para esta origem local. Os parâmetros de consulta
são omitidos dos logs porque as URLs podem conter tokens assinados. A sondagem mede o caminho de
um único processo, não a latência dos usuários nem uma taxa global de acertos do cache. O fetch do
Node decodifica corpos comprimidos, então essa contagem de bytes não representa os bytes
comprimidos transmitidos pela rede nem uma métrica de cobrança. O cabeçalho
cf-cache-status é específico da Cloudflare; adapte-o ao seu provedor. Cada sondagem
adiciona tráfego que pode ser contabilizado nas requisições e na transferência.
Estratégias para otimizar os custos de CDN
Use a planilha para escolher a mudança: representações menores reduzem os bytes entregues; o cache de borda reduz o trabalho na origem; o cache do navegador reduz requisições repetidas pela rede. Verifique uma mudança pela métrica de cobrança correspondente, em vez de presumir que as quatro categorias de custo diminuem juntas.
Minimizar transferências de dados desnecessárias
Para testar a compressão na origem, adicione este código a server.mts
imediatamente após const app = express() e reinicie o servidor usando
PORT=0 node server.mts. A importação permanece no escopo do módulo. Esta é uma alternativa
quando a compressão ainda não é fornecida em outro ponto do caminho de entrega.
import compression from 'compression'
app.use(compression({ level: 6, threshold: 0 }))
Requisite /assets/app.js com Accept-Encoding: gzip e verifique
Content-Encoding, a contagem de bytes do corpo comprimido e os bytes decodificados em
comparação com o original. Salve este código como check-compression.mts e execute
node check-compression.mts "$DEMO_ORIGIN" após atualizar DEMO_ORIGIN com a URL impressa pelo
servidor reiniciado. Ele lê bytes HTTP brutos porque o fetch os decodificaria para você:
import { strict as assert } from 'node:assert'
import { readFile } from 'node:fs/promises'
import { get } from 'node:http'
import { gunzipSync } from 'node:zlib'
const url = new URL('/assets/app.js', process.argv[2])
const response = await new Promise((resolve, reject) => {
get(url, { headers: { 'Accept-Encoding': 'gzip' }, signal: AbortSignal.timeout(5000) }, resolve)
.on('error', reject)
})
const chunks = []
for await (const chunk of response) chunks.push(chunk)
assert.equal(response.statusCode, 200)
assert.equal(response.headers['content-encoding'], 'gzip')
const encoded = Buffer.concat(chunks)
const original = await readFile('dist/assets/app.js')
assert.equal(Buffer.compare(gunzipSync(encoded), original), 0, 'Decoded bytes differ from the source')
console.log(`${encoded.length}B gzip, ${original.length}B decoded`)
Não aplique gzip novamente a JPEG, WebP ou AVIF só porque são grandes. O
middleware de compressão filtra por tipo de conteúdo e respeita
no-transform; a configuração de compressão da sua CDN pode ser diferente. Este
laboratório não comprova economia com compressão para seus recursos de produção.
Usar o cache de forma eficaz
Normalize apenas parâmetros de consulta comprovadamente irrelevantes para o objeto. Aqui,
w=small se torna um segmento do caminho e utm_source pode
ser descartado; version, crop e tokens assinados não
podem. A função auxiliar recusa essas entradas desconhecidas em vez de mapeá-las para outro objeto.
Acompanhe separadamente a taxa de acertos por requisição e a taxa de acertos por byte, e inspecione
quais variantes ou valores de consulta causam ausências no cache antes de alterar a chave de cache.
Conclusão
Use os dados de um mês sobre requisições, regiões, transferência e origem para calcular os custos com a tabela de tarifas escolhida; depois, compare com outra estimativa após a mudança que você mediu. Se sua aplicação precisar de uma etapa de processamento de arquivos durante a compilação, a Transloadit pode fornecer essa parte do fluxo de trabalho; a política de cache e a cobrança do provedor ainda precisam de verificações próprias.
