Developer Tools

Testing API Performance in the Terminal: Curl vs. HTTPie vs. Bruno CLI

By DexNox Dev Team Published May 9, 2026

When debugging an API endpoint, checking service latency, or writing integration tests, desktop graphical interfaces add operational friction. Developers must switch contexts, navigate complex menus, and cannot easily automate sequences or store requests in Git version control systems. Terminal API clients—curl, HTTPie, and Bruno CLI—provide alternative methods for interacting with HTTP APIs, representing different tradeoffs between baseline ubiquity and developer experience.

Understanding the Network Metrics of API Requests

When evaluating API performance in the terminal, it is helpful to analyze the distinct phases of an HTTP connection. A request is not a single transaction; it is a sequence of network operations:

  1. DNS Lookup (time_namelookup): The time required to contact DNS servers and translate the hostname into an IP address.
  2. TCP Connection (time_connect): The duration of the three-way handshake required to establish a TCP channel between the client host and the remote server.
  3. TLS/SSL Handshake (time_appconnect): The time required to exchange certificates, negotiate cipher suites, and establish cryptographic tunnels.
  4. Time to First Byte (TTFB - time_starttransfer): The duration from connection initialization until the remote web server returns the first byte of response data, representing server-side processing latency.
  5. Content Transfer (time_total minus time_starttransfer): The time required to download the full payload over the established socket network.
Connection Timeline:
├─► DNS Lookup (time_namelookup)
│   ├─► TCP Handshake (time_connect)
│   │   ├─► TLS Handshake (time_appconnect)
│   │   │   ├─► TTFB / Server Processing (time_starttransfer)
│   │   │   │   ├─► Content Download (time_total)

Understanding these latency boundaries allows you to isolate performance issues. If a response is slow, checking these metrics reveals whether the latency is caused by a slow database query (high TTFB) or a slow network connection (high content transfer time).

curl: The Universal Diagnostic Client

curl is installed by default on Unix systems and is universally available in shell scripts. Its flags can be verbose, but it offers precise control over connection parameters and protocol features.

Installation

# macOS via Homebrew (installs updated versions with HTTP/3 support)
brew install curl

# Ubuntu/Debian Linux environments
sudo apt install curl

# Check active protocols and version strings
curl --version

Common Request Patterns

# GET request with Bearer authentication and JSON output formatting
curl -sS \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Accept: application/json" \
  https://api.example.com/users | jq .

# POST request passing a JSON payload
curl -sS -X POST \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Chen", "email": "alice@example.com", "role": "admin"}' \
  https://api.example.com/users | jq .

# POST request performing a multipart file upload
curl -sS -X POST \
  -H "Authorization: Bearer $API_TOKEN" \
  -F "file=@./report.pdf" \
  -F "description=Monthly report" \
  https://api.example.com/uploads

# PUT request loading JSON payload from a local file
curl -sS -X PUT \
  -H "Content-Type: application/json" \
  -d @payload.json \
  https://api.example.com/users/42

# GraphQL query execution
curl -sS -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $API_TOKEN" \
  -d '{"query": "{ users { id name } }"}' \
  https://api.example.com/graphql | jq '.data.users'

# Force HTTP/2 protocol mapping
curl --http2 -sS https://api.example.com/status

HTTPie: Ergonomic Command Line Testing

HTTPie provides an ergonomic alternative to curl. It includes automatic JSON serialization, syntax-highlighted responses, and intuitive request parameters by default.

Installation

# macOS via Homebrew
brew install httpie

# Linux environments
sudo apt install httpie

# Check installation status
http --version

Common Request Patterns

# GET request with automated response highlighting
http https://api.example.com/users \
  Authorization:"Bearer $API_TOKEN"

# POST request with JSON parameters (HTTPie defaults to JSON content)
http POST https://api.example.com/users \
  Authorization:"Bearer $API_TOKEN" \
  name="Alice Chen" \
  email="alice@example.com" \
  role="admin"

# POST request with nested JSON variables using raw value assignments (:=)
http POST https://api.example.com/users \
  name="Alice Chen" \
  metadata:='{"source": "cli", "verified": true}' \
  tags:='["admin", "billing"]'

# Multipart file upload
http -f POST https://api.example.com/uploads \
  Authorization:"Bearer $API_TOKEN" \
  file@./report.pdf \
  description="Monthly report"

# Show request and response headers only
http -h GET https://api.example.com/users

Bruno CLI: Version-Controlled Collections

Bruno stores API requests in folders as human-readable .bru files, making them easy to track in Git, review in pull requests, and run in CI/CD pipelines via @usebruno/cli.

Installation

# Install the Node.js runner utility globally
npm install -g @usebruno/cli

Example .bru Request Declaration

meta {
  name: Create User
  type: http
  seq: 1
}

post {
  url: {{baseUrl}}/users
  body: json
  auth: bearer
}

auth:bearer {
  token: {{authToken}}
}

headers {
  Content-Type: application/json
}

body:json {
  {
    "name": "{{userName}}",
    "email": "{{userEmail}}",
    "role": "member"
  }
}

assert {
  res.status: eq 201
  res.body.id: isDefined
}

Running Collections in Headless Mode

# Execute all requests inside the users directory
bru run users/ --env local

# Generate test results report in JUnit XML format for CI engines
bru run api-tests/ --env staging --reporter junit --output report.xml

Tool Feature Matrix

Functional Metriccurl CLIHTTPie ClientBruno CLI runner
Command VerbosityHigh (Requires manual header flags)Low (Implicit formats and parameters)Low (Stored inside config files)
JSON SerializationManual serialization requiredAutomatic JSON structures parsingBuilt-in UI and file mapping
Git CompatibilityShell scripts onlyShell scripts onlyNative .bru text files
Header PrintingNeeds -i or -v flagsPrinted by defaultPrinted inside test reports
Environment SetsManaged via shell variablesManaged via shell variablesNative .bru environment files

What Breaks in Production: Failure Modes and Mitigations

Using terminal API clients in local scripts and CI pipelines introduces several runtime risks.

1. Shell Parameter Expansion Breaks JSON Syntax

When executing API calls containing inline JSON strings from Zsh, Bash, or PowerShell, double and single quoting rules vary across operating systems.

  • Failure Mode: Declaring a payload like '{"name": "$USER"}' works on macOS/Linux, but Windows CMD parses the single quotes literally, sending corrupted JSON payload strings to the server and causing a 400 Bad Request error.
  • Mitigation: Use local payload files with the -d @filename.json syntax in curl, or let HTTPie handle serialization natively using parameter assignments (name="$USER").

2. Secret Exposure inside Shell History Logs

Executing commands with sensitive tokens inline (such as passing auth keys in the header) writes the secrets to the system history logs.

  • Failure Mode: Tokens are saved in plain text in ~/.zsh_history or ~/.bash_history, where they can be read by other processes or exposed in system backups.
  • Mitigation: Use environment variables (e.g., Authorization: Bearer $SECRET_TOKEN) instead of writing raw keys, or load variables dynamically from files using configuration managers.

3. Buffered Outputs Block EventStreams (SSE)

When checking streaming endpoints (such as Server-Sent Events or WebSockets connections), curl and HTTPie buffer network packets locally to optimize write operations.

  • Failure Mode: The terminal remains blank, and events are not displayed in real time. The client only prints output when the buffer fills up or the connection closes, making it difficult to debug live streams.
  • Mitigation: Disable write buffering. Pass -N or --no-buffer in curl, or run HTTPie with the --stream flag.

4. Silent Connection Success on Error Status Codes

In automated integration check scripts, developers use curl commands to query health check endpoints.

  • Failure Mode: If the server fails and returns an HTTP 500 Internal Server Error, curl still exits with status code 0 because a connection was successfully established. This allows failing builds to pass in CI pipelines.
  • Mitigation: Enforce exit status errors on non-2xx codes. Use -f or --fail in curl to return exit status 22 on server errors, or run http with --check-status in HTTPie.

Frequently Asked Questions

How do I prevent curl from saving API tokens to my bash or zsh history files?

You can execute your terminal commands with a leading space (e.g., curl -H "Authorization..."). Most default Unix shells are configured to skip logging commands that start with a space to prevent writing sensitive credentials to history logs.

Why does my curl command hang when testing an HTTP/3 endpoint locally?

Most standard, pre-installed system curl binaries do not support HTTP/3 or QUIC. Attempting to force connections using --http3 on an unsupported binary will fail or fallback to slower TCP protocols. You must compile curl with SSL engines like quiche or ngtcp2 to enable HTTP/3 support.

How can I make curl fail with a non-zero exit code when the API returns a 500 error?

Pass the --fail or -f flag to the command. This tells curl to terminate and return exit code 22 if the server returns an HTTP status code greater than or equal to 400.

What is the easiest way to inspect raw HTTP request headers sent by HTTPie?

Run your request with the --print=Hh flag (or -p Hh). This instructs HTTPie to output both the request headers (H) and the response headers (h) directly to the console before printing the response body.

How do I use client certificates for SSL authentication in curl and HTTPie?

For certificate-based authentication, pass the certificate and key files directly. In curl, use the --cert and --key flags:

curl --cert client.crt --key client.key https://api.example.com/secure

In HTTPie, use the --cert and --cert-key flags:

http --cert=client.crt --cert-key=client.key https://api.example.com/secure

Can I mock requests with custom User-Agent headers to debug CDN routing issues?

Yes. CDNs and load balancers often route traffic or apply rate limits based on user-agent strings. You can override the default headers easily. In curl, pass the -A flag:

curl -A "CustomCDNTestSuite/1.0" https://api.example.com/data

In HTTPie, override the User-Agent key directly:

http https://api.example.com/data User-Agent:"CustomCDNTestSuite/1.0"

Wrapping Up

curl remains the standard client for universal compatibility in shell environments. HTTPie provides an ergonomic developer experience, making it a good choice for manual API exploration. Bruno CLI is designed for team environments, allowing collections to be stored in version control and executed headless in CI pipelines.

Frequently Asked Questions

How do you send raw JSON requests using HTTPie?

Use colon separators for key-value pairs: `http POST api.example.com/users name='Alice' role='admin'`. HTTPie automatically serializes these into a JSON request body with Content-Type: application/json. For nested objects, use := for raw JSON values: `http POST api.example.com name='test' meta:='{"created":true}'`.

Does curl support HTTP/3 calls natively?

Yes, newer curl versions built with quiche or ngtcp2 support HTTP/3 via QUIC using the `--http3` flag. Most system-installed curl binaries on macOS and Linux do not include HTTP/3 support by default. Check with `curl --version | grep HTTP3`. Homebrew's curl and the official curl Docker image include HTTP/3 support.

Can Bruno CLI run API collections in CI without a GUI?

Yes. The `@usebruno/cli` package provides the `bru run` command that executes .bru collection files in headless mode. It supports environment variables, output formats (JSON, JUnit XML), and exit codes for pass/fail signaling—making it suitable for GitHub Actions and other CI pipelines.

How do I test API authentication flows with Bearer tokens in these tools?

All three support Bearer tokens. In curl: `-H 'Authorization: Bearer YOUR_TOKEN'`. In HTTPie: `http GET api.example.com/data 'Authorization:Bearer YOUR_TOKEN'` or `http -A bearer -a YOUR_TOKEN GET api.example.com/data`. In Bruno, set the Auth type to Bearer in the request's Auth tab—it stores the token in the environment variables file.