How to Use cURL PUT Requests: Beyond the Basics

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:

  1. Implement conditional updates with ETags to prevent overwriting concurrent changes
  2. Build a wrapper script that handles authentication, retries, and logging automatically
  3. 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.

Marius Bernard

Marius Bernard

Marius Bernard is a Product Advisor, Technical SEO, & Brand Ambassador at Roundproxies. He was the lead author for the SEO chapter of the 2024 Web and a reviewer for the 2023 SEO chapter.