15 out 2026 · ferramentas, linux, api · 6 min
O que dá para medir com curl em uma chamada
Uma requisição https para um host público levou 63,3 ms na média de dez chamadas. O handshake TLS sozinho tomou 41,5 ms disso, mais que a metade. Nenhuma dessas cinco fases apareceu no -w quando eu pedi só o total. Separar a chamada por fase é o que transforma 63 ms em uma decisão.
O -w é o instrumento
O -w escreve uma linha de medição no lugar de falar com o servidor. Cada %{...} é uma variável do próprio curl. As cinco fases vêm de uma chamada só:
curl -sS -o /dev/null \
-w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect}\nttfb=%{time_starttransfer} total=%{time_total}\n' \
https://exemplo.org
O -o /dev/null joga o corpo fora, o -sS cala o progresso mas deixa o erro aparecer. As variáveis saem acumuladas, como um cronômetro que não zera: time_connect já contém o DNS. Para ver a parcela de cada uma, subtraia.
O dns custou 3,0 ms, a conexão tcp mais 8,7 ms, o tls mais 29,8 ms, e o servidor mais 21,7 ms. Se a sua API está lenta, esse degrau final é o número que interessa. Se ela responde rápido e a lentidão aparece do outro lado da rede, o handshake é o suspeito.
O mesmo raciocínio de decompor em fases, mas medindo em vez de adivinhar, está no post dez imports custam 100 ms na partida.
O artefato: seis receitas em um script
O medir-api.sh abaixo roda as seis medições contra qualquer API que responda nas rotas /saude, /eco, /vai e /quebra. Ele não instala nada, não escreve arquivo e sai com código 0 mesmo quando o curl não está presente.
#!/usr/bin/env bash
# medir-api.sh: as receitas de curl que valem para qualquer API.
#
# BASE=http://127.0.0.1:8099 bash medir-api.sh
#
# O script roda seis medicoes: tempo por fase, cabecalho de resposta,
# HEAD sem corpo, redirect com e sem -L, POST em json e erro com -f.
# Ele nao instala nada e nao escreve arquivo.
BASE="${BASE:-https://example.org}"
SAUDE="${SAUDE:-/saude}"
ECO="${ECO:-/eco}"
VAI="${VAI:-/vai}"
ERRO="${ERRO:-/quebra}"
# As cinco fases do curl, acumuladas como ele imprime.
FMT='dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n'
precisa_de_curl() {
if ! command -v curl >/dev/null 2>&1; then
echo "curl nao esta instalado neste ambiente."
echo "em Debian ou Ubuntu: apt-get install -y curl"
echo "em Alpine: apk add curl"
return 1
fi
return 0
}
precisa_de_curl || exit 0
echo "== 1. tempo por fase =="
curl -sS -o /dev/null -w "$FMT" "$BASE$SAUDE"
echo
echo "== 2. cabecalho de resposta, sem o corpo =="
curl -sS -D - -o /dev/null "$BASE$SAUDE"
echo
echo "== 3. HEAD: so o tamanho declarado =="
curl -sS -I "$BASE$SAUDE" | grep -i 'content-length' | head -1
echo
echo "== 4. redirect: sem -L e com -L =="
curl -sS -o /dev/null -w 'code=%{http_code} redirects=%{num_redirects} total=%{time_total}\n' "$BASE$VAI"
curl -sS -L -o /dev/null -w 'code=%{http_code} redirects=%{num_redirects} total=%{time_total}\n' "$BASE$VAI"
echo
echo "== 5. POST em json, e quantos bytes sairam =="
curl -sS -X POST "$BASE$ECO" \
-H 'Content-Type: application/json' \
-d '{"nome":"joseti","tentativa":1}' \
-w '\nenviados=%{size_upload} total=%{time_total}\n'
echo
echo "== 6. erro vira codigo de saida =="
curl -sS -o /dev/null -w 'code=%{http_code}\n' "$BASE$ERRO"
curl -sS -f -o /dev/null "$BASE$ERRO"
echo "com -f o curl saiu com codigo $?"
Rodando contra um servidor de teste que responde 200, devolve 302 numa rota e 500 numa rota de erro, a saída foi de 0,911 ms na primeira medição, cabeçalho com Content-Length: 50, o mesmo 50 no HEAD, redirects=0 sem -L contra redirects=1 com ele, 31 bytes enviados no POST e código 22 no final.
POST em json, com o arquivo quando o corpo é grande
O -H 'Content-Type: application/json' não é decoração: sem ele o curl manda application/x-www-form-urlencoded e a API rejeita o corpo.
curl -sS -X POST https://exemplo.org/api/eco \
-H 'Content-Type: application/json' \
-d '{"nome":"joseti","tentativa":1}'
Para corpo grande ou com acento, use --data-binary @arquivo. Com -d @arquivo, o curl tira as quebras de linha e os retornos de carro do arquivo antes de enviar, e o Content-Length sai errado. Com --data-binary, nenhum byte muda.
curl -sS -X POST https://exemplo.org/api/eco \
-H 'Content-Type: application/json' \
--data-binary @carga.json
Para confirmar quantos bytes realmente saíram, junte com -w 'enviados=%{size_upload}\n'. Foi assim que conferi 2.258 bytes num arquivo de exemplo.
Ver o cabeçalho, e só o cabeçalho
-D - despeja os cabeçalhos da resposta em stdout e -o /dev/null segura o corpo. Com -sS e -D -, a saída cabe numa tela:
curl -sS -D - -o /dev/null https://exemplo.org/api/saude
Para só o tamanho declarado, curl -sI url | grep -i content-length faz o pedido HEAD, que o servidor responde sem enviar corpo nenhum. É a forma barata de checar se o que está no CDN é o que você espera.
Redirect: o -L dobra o tempo e some do número
Um 302 seguido, medido contra servidor local, deu 0,70 ms sem -L e 1,22 ms com -L. Com o -L, o curl faz a segunda chamada, mas o total impresso é o da última. Quem compara com o número de antes está comparando coisas diferentes.
curl -sS -o /dev/null -w 'code=%{http_code} redirects=%{num_redirects}\n' https://exemplo.org/vai
curl -sS -L -o /dev/null -w 'code=%{http_code} redirects=%{num_redirects} total=%{time_total}\n' https://exemplo.org/vai
Sempre imprima %{num_redirects} junto do code. Um 301 que virou 302 é o tipo de coisa que passa semanas sem ninguém olhar.
Deixar o erro virar código de saída
Sem -f, um 500 vem com exit 0 e parece sucesso. Com -f, o curl sai com 22 e o set -e do script para. Foi o que aconteceu no teste contra a rota de erro.
curl -sS -f https://exemplo.org/api/saude || echo "a chamada falhou"
Tabela
| Receita | O que resolve |
|---|---|
-w |
ver o tempo por fase |
-D - |
ver cabeçalho de resposta |
-I |
só cabeçalho, sem corpo |
-L |
seguir redirect |
-f |
erro HTTP vira saída 22 |
--data-binary @arq |
corpo grande sem perder byte |
Se der errado
-w não mostra nada além do total. Causa: você escreveu %{time_*} com barra invertida fora do par de aspas. Correção: -w só imprime se a string estiver entre aspas duplas.
A resposta vem com quebras de linha e fica ilegível no shell. Causa: o corpo veio junto. Correção: -o /dev/null quando você só quer o -w.
curl: (60) SSL certificate problem. Causa: cadeia incompleta no sistema ou proxy no meio. Correção: -k só para testar, nunca em script que vai para produção.
FAQ
-s esconde o erro também? Esconde. -sS cala o progresso e mantém o erro no stderr, e é esse que você quer.
Como salvo a resposta num arquivo? -o saida.json, e com -w junto o -o recebe só o corpo. Para ver e gravar ao mesmo tempo, use --dump-header.
--compressed faz diferença? Faz, quando a resposta vem em gzip. Ele pede a descompressão automática e o %{size_download} passa a contar os bytes já descompactados.
Dá para usar isso dentro de script sem instalar nada? O curl já vem na maioria das imagens Debian e Ubuntu. A imagem python:3-slim também tem, e a alpine precisa de apk add curl.
E httpie, wget ou xh? Wget resolve download e arquivo grande; httpie tem sintaxe mais bonita para humano. O curl ganha quando o script precisa do tempo por fase, do código de saída e do cabeçalho na mesma chamada.
Fontes
manual do curl e o capítulo de HTTP do guia do projeto, conferidos na versão 8.23. Cada variável do -w está documentada com nome na própria página, vale abrir antes de inventar uma nova. Para o outro lado da latência, quando o problema é o tempo do modelo e não o da rede, o post avaliando se o modelo piorou mostra o método.