cURL PUT requests let you update resources on servers through the command line, but most tutorials stop at the basics.
This guide digs into the tricks that actually matter – from handling stubborn 411 errors to implementing smart retry mechanisms when APIs fight back.
You've probably used GET and POST requests a thousand times. But PUT requests? They're the unsung workhorses of RESTful APIs, perfect for updating entire resources idempotently. While everyone defaults to POST, PUT shines when you need guaranteed consistency – upload the same file ten times, and you'll get the same result, not ten copies.
Step 1: Start with the Fundamentals (But Skip the Fluff)
The basic PUT syntax looks deceptively simple:
curl -X PUT https://api.example.com/resource/123 -d "data"
But here's what most guides won't tell you: the -X PUT
flag is often redundant. When you use certain options like -T
for file uploads, curl automatically switches to PUT:
# These are equivalent for file uploads
curl -T myfile.json https://api.example.com/resource/123
curl -X PUT --data-binary @myfile.json https://api.example.com/resource/123
The real question is: which should you use? Go with -T
for file uploads – it handles binary data better and doesn't load everything into memory first.
The JSON Trap
Sending JSON? Don't forget the Content-Type header, or you'll spend hours debugging why your perfectly valid JSON gets rejected:
curl -X PUT https://api.example.com/users/42 \
-H "Content-Type: application/json" \
-d '{"name":"John","age":30}'
Pro move: Use --data-raw
instead of -d
when your JSON contains special characters. The regular -d
flag strips newlines and can mangle your data.
Step 2: Handle Authentication Like You Mean It
Bearer Tokens Without the Hassle
Most APIs use Bearer tokens these days. Here's the clean way to handle them:
# Store your token in an environment variable
export API_TOKEN="your_token_here"
# Use it in your requests
curl -X PUT https://api.example.com/resource \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status":"updated"}'
But what if your token expires mid-script? Here's a pattern that auto-refreshes:
#!/bin/bash
refresh_token() {
TOKEN=$(curl -s -X POST https://api.example.com/auth/refresh \
-d "refresh_token=$REFRESH_TOKEN" | jq -r '.access_token')
export API_TOKEN=$TOKEN
}
make_put_request() {
response=$(curl -w "\n%{http_code}" -X PUT https://api.example.com/resource \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d "$1")
http_code=$(echo "$response" | tail -n1)
if [ "$http_code" = "401" ]; then
refresh_token
make_put_request "$1" # Retry with new token
else
echo "$response" | head -n-1
fi
}
Multiple Auth Headers? No Problem
Some APIs require both basic auth for the server and bearer tokens for the application:
curl -X PUT https://api.example.com/resource \
-u "username:password" \
-H "X-API-Token: Bearer $TOKEN" \
-d @data.json
The trick? Use a custom header like X-API-Token
when you need both. Most servers will accept this without complaint.
Step 3: Conquer the Dreaded 411 Error
Getting "411 Length Required" errors? You're not alone. This happens when servers demand a Content-Length header but curl doesn't provide one. Three fixes that actually work:
Fix 1: Add Empty Data
# Instead of this (which might fail)
curl -X PUT https://api.example.com/resource
# Do this
curl -X PUT https://api.example.com/resource -d ''
Fix 2: Calculate Content-Length Manually
data='{"key":"value"}'
length=${#data}
curl -X PUT https://api.example.com/resource \
-H "Content-Length: $length" \
-d "$data"
Fix 3: Use Chunked Transfer (When the Server Supports It)
curl -X PUT https://api.example.com/resource \
-H "Transfer-Encoding: chunked" \
-T large_file.bin
Step 4: Upload Large Files Without Drama
Uploading a 4GB file? Standard approaches will eat your RAM for breakfast. Here's how to do it right:
Stream Large Files
# DON'T do this (loads entire file into memory)
curl -X PUT https://api.example.com/upload --data-binary @huge_file.iso
# DO this instead (streams the file)
curl -T huge_file.iso https://api.example.com/upload
Split and Conquer
For truly massive files, split them first:
# Split into 100MB chunks
split -b 100M large_file.bin chunk_
# Upload each chunk with position headers
offset=0
for chunk in chunk_*; do
size=$(stat -f%z "$chunk" 2>/dev/null || stat -c%s "$chunk")
end=$((offset + size - 1))
curl -X PUT "https://api.example.com/upload?partNumber=$chunk" \
-H "Content-Range: bytes $offset-$end/*" \
-T "$chunk"
offset=$((end + 1))
done
Compress on the Fly
Why send 1GB when you can send 100MB?
# Compress and upload in one go
gzip -c large_file.json | curl -X PUT https://api.example.com/resource \
-H "Content-Encoding: gzip" \
-H "Content-Type: application/json" \
--data-binary @-
Step 5: Build Bulletproof Retry Logic
APIs fail. Networks hiccup. Here's how to handle it gracefully:
Smart Exponential Backoff
curl --retry 5 \
--retry-delay 1 \
--retry-max-time 60 \
--retry-all-errors \
-X PUT https://flaky-api.example.com/resource \
-d @data.json
But curl's built-in retry is limited. For production-grade retries with rate limit handling:
#!/bin/bash
put_with_retry() {
local url=$1
local data=$2
local max_retries=5
local retry_count=0
local delay=1
while [ $retry_count -lt $max_retries ]; do
response=$(curl -s -w "\n%{http_code}" -X PUT "$url" \
-H "Content-Type: application/json" \
-d "$data")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | head -n-1)
case $http_code in
200|201|204)
echo "$body"
return 0
;;
429)
# Check for Retry-After header
retry_after=$(curl -sI -X PUT "$url" | grep -i "retry-after:" | cut -d' ' -f2 | tr -d '\r')
if [ -n "$retry_after" ]; then
delay=$retry_after
fi
echo "Rate limited. Waiting $delay seconds..." >&2
;;
5*)
echo "Server error. Retrying in $delay seconds..." >&2
;;
*)
echo "Request failed with code $http_code" >&2
echo "$body" >&2
return 1
;;
esac
sleep $delay
delay=$((delay * 2)) # Exponential backoff
retry_count=$((retry_count + 1))
done
echo "Max retries exceeded" >&2
return 1
}
The PUT vs PATCH Dilemma
Here's when to actually use PUT vs PATCH:
Use PUT when:
- Replacing entire resources
- You need idempotency guarantees
- Working with simple objects
- The API doesn't support PATCH
Use PATCH when:
- Updating specific fields
- Dealing with large objects where sending everything is wasteful
- You need to increment counters or apply relative changes
Real-world example – updating user profile:
# PUT - Send everything (even unchanged fields)
curl -X PUT https://api.example.com/users/123 \
-H "Content-Type: application/json" \
-d '{"name":"John","email":"john@example.com","age":30,"city":"NYC"}'
# PATCH - Send only what changed
curl -X PATCH https://api.example.com/users/123 \
-H "Content-Type: application/json-patch+json" \
-d '[{"op":"replace","path":"/age","value":31}]'
Bonus Tricks That Save Hours
Debug Without the Noise
# See exactly what curl sends/receives
curl -X PUT https://api.example.com/resource \
--trace-ascii - \
-d "test" 2>&1 | grep -E "^(>|<)"
Handle Redirects Properly
# Follow redirects but maintain PUT method
curl -L -X PUT https://api.example.com/resource \
-d @data.json \
--post301 --post302 --post303
Use Proxies for Debugging
# Route through local proxy to inspect traffic
curl -X PUT https://api.example.com/resource \
-x localhost:8080 \
-k # Ignore SSL errors from proxy
-d @data.json
Test Idempotency
# Run the same PUT request 3 times and compare responses
for i in {1..3}; do
echo "Request $i:"
curl -X PUT https://api.example.com/resource/999 \
-H "Content-Type: application/json" \
-d '{"status":"active"}' \
-s | md5sum
done
# If all checksums match, your PUT is properly idempotent
Common Pitfalls and Their Fixes
Empty Response with Large Files? Your server might be timing out. Add --expect100-timeout 0
to disable the Expect header:
curl -X PUT https://api.example.com/upload \
--expect100-timeout 0 \
-T large_file.bin
Getting 415 Unsupported Media Type? You forgot the Content-Type or sent the wrong one:
# Check what the server accepts
curl -X OPTIONS https://api.example.com/resource -i
# Then match it exactly
curl -X PUT https://api.example.com/resource \
-H "Content-Type: application/vnd.api+json" \
-d @data.json
PUT Creating Instead of Updating? Some APIs use PUT for both. Check the response code:
- 200/204 = Updated existing resource
- 201 = Created new resource
Next Steps
Now that you've mastered PUT requests, consider these advanced patterns:
- Implement conditional updates with ETags to prevent overwriting concurrent changes
- Build a wrapper script that handles authentication, retries, and logging automatically
- Explore HTTP/2 multiplexing for parallel PUT requests using
--http2
The difference between junior and senior developers isn't knowing PUT exists – it's knowing when -T
beats --data-binary
, why chunked encoding matters, and how to handle 429s without breaking a sweat.
Frequently Asked Questions
What's the difference between cURL PUT and POST requests?
PUT requests are idempotent and designed to update or replace entire resources at a specific URL. Running the same PUT request multiple times produces the same result. POST requests create new resources and aren't idempotent – running the same POST multiple times creates multiple resources. Use PUT when you know the resource's exact location and want to update it completely.
How do I send JSON data with a cURL PUT request?
Always include the Content-Type header when sending JSON: curl -X PUT https://api.example.com/resource -H "Content-Type: application/json" -d '{"key":"value"}'
. Use --data-raw
instead of -d
if your JSON contains special characters, and consider using -d @file.json
to read from a file instead of inline JSON.
Can I use cURL PUT to upload large files without running out of memory?
Yes, use the -T
flag instead of --data-binary @filename
. The -T
option streams the file rather than loading it entirely into memory. For files over 2GB, you might also need to use chunked transfer encoding with -H "Transfer-Encoding: chunked"
if the server supports it.
When should I use PUT vs PATCH in cURL?
Use PUT when replacing an entire resource – you must send all fields, even unchanged ones. Use PATCH for partial updates when you only want to modify specific fields. PUT is always idempotent (safe to retry), while PATCH may not be. Not all servers support PATCH, making PUT the more universal choice despite being less efficient for small updates.