Sunday, 9 August 2026

Images, PDFs, and High-volume processing

 Images are not free in terms of context budget.

There are 3 patterns to process image to send to Claude : 

base64, a URL, or a Files API file_id

Base64 — most commonly used

Despite being the most verbose option, base64 dominates in production. The reason is simplicity — no external dependency, no URL management, no file storage infrastructure. You read the file, encode it, send it. Works everywhere, every time.

java
// Straightforward in Java — fits naturally into existing Spring Boot code
byte[] fileBytes = Files.readAllBytes(path);
String base64Data = Base64.getEncoder().encodeToString(fileBytes);

Common in: document processing pipelines, support ticket systems, invoice extractors, any backend that receives files via multipart upload and immediately processes them.


Files API (file_id) — growing fast, strong production case

Best choice when the same file is processed multiple times. You upload once, get a file_id, reuse it across requests without re-encoding and re-transmitting the same bytes repeatedly.

Upload PDF once    → file_id: file_01ABC
Request 1: analyse this file_id   → no re-upload
Request 2: summarize this file_id → no re-upload
Request 3: extract fields         → no re-upload

Common in: document management systems, legal review tools, any workflow where the same document is processed in multiple passes. The cost saving on repeated processing is significant — you're not paying to transmit the same large PDF on every API call.


URL — least common in production

Sounds convenient but has real problems in production:

- URL must be publicly accessible         → security concern
- Anthropic's servers fetch it directly   → your internal docs not reachable
- URL can expire or go offline            → reliability risk
- No control over when Anthropic fetches  → timing issues

Used mainly in: quick prototypes, demos, or public-facing content processing where the document is already publicly hosted. Rarely the right choice for enterprise production systems where documents are private.


Honest recommendation for your context

Given your Spring Boot / enterprise background:

Single-pass processing    → base64
                            simple, no infrastructure, fits your existing upload flow

Multi-pass same document  → Files API
                            upload once, reuse file_id, save bandwidth and token cost

Public documents already  → URL
hosted online               only if the document is already public and stable

Most mature production systems start with base64 and migrate specific high-volume document flows to Files API once the reuse pattern becomes clear.


In Files API (file_id) approach where to upload?

You upload to Anthropic's servers via a dedicated Files API endpoint:

POST https://api.anthropic.com/v1/files

This is a separate API call — not part of a messages call. You send the file bytes to Anthropic, they store it on their infrastructure, and they hand you back a file_id.


Who provides the file_id

Anthropic provides it in the response to your upload call:

java
// Step 1: Upload the file to Anthropic
POST /v1/files
Content-Type: multipart/form-data
Body: { file: <your PDF bytes> }

// Anthropic responds with:
{
  "id":         "file_01ABC",Anthropic generates this
  "filename":   "invoice.pdf",
  "size":       245678,
  "created_at": 1234567890
}

You store that file_id on your side — in your database, Redis, wherever makes sense for your application.


Step 2: Use the file_id in subsequent Messages API calls

java
// No re-upload needed — just reference the file_id
{
  "model": "claude-sonnet-4-6",
  "messages": [{
    "role": "user",
    "content": [
      {
        "type": "document",
        "source": {
          "type":    "file",
          "file_id": "file_01ABC"Anthropic fetches it from their own storage
        }
      },
      {
        "type": "text",
        "text": "Extract all line items from this invoice"
      }
    ]
  }]
}

The full flow in your Spring Boot context:

Your Application                    Anthropic
────────────────                    ──────────────────────────
User uploads PDF
    ↓
POST /v1/files          →           Receives PDF bytes
                                    Stores on their servers
                        ←           Returns file_01ABC

Store file_01ABC
in your DB
    ↓
User requests           →           Receives messages request
analysis                            Fetches PDF from own storage
                                    Processes it
                        ←           Returns analysis

User requests           →           Receives messages request
summary                             Fetches same PDF from storage
                                    Processes it
                        ←           Returns summary

Two important practical points:

File lifetime — Anthropic doesn't store your files forever. Files expire after a fixed period (currently 30 days). If your use case needs the file longer than that, you need to re-upload or manage expiry on your side.

Storage is not free — Storing files on Anthropic's infrastructure has its own cost separate from API token costs. Worth factoring in if you're processing high volumes of large documents.


Bottom line

You upload to Anthropic via /v1/files. Anthropic stores it and gives you back a file_id. You store that ID in your own database. Every subsequent Messages API call references the ID — Anthropic fetches the file from their own storage without you transmitting it again.


High-volume processing

The Message Batch API takes up to 100,000 requests or 256 MB per batch in a single batch call, returns a batch_id, and processes them asynchronously.

The fundamental difference is who does the work and when

Synchronous loop (10,000 calls):
─────────────────────────────────
Your code:    send ticket-1  → wait → get result → send ticket-2 → wait → get result...
Your server:  blocked for hours, holding connections open
Anthropic:    sees 10,000 live requests demanding immediate responses
Rate limiter: FIRES

Batch API (1 call):
──────────────────
Your code:    send all 10,000 as a manifest → get batch_id → move on, do other things
Your server:  free immediately, not blocked
Anthropic:    queues 10,000 items, processes them at their own pace over hours
Rate limiter: never fires — no live requests demanding immediate response

You are submitting 10,000 independent classification requests — each one is a completely separate Claude conversation with its own input.


Concrete example — support ticket classification:

Your database has 10,000 support tickets:
ticket-1: "I was charged twice for April"
ticket-2: "API returning 429 error"
ticket-3: "Cannot login to my account"
...
ticket-10000: "Where is my refund?"

Each ticket needs to be independently classified as BILLING, TECHNICAL, or ESCALATION. These are 10,000 completely separate tasks — not one big task.


What you actually send in the batch call:

json
{
  "requests": [
    {
      "custom_id": "ticket-1",
      "params": {
        "model": "claude-sonnet-4-6",
        "messages": [{"role": "user", 
                      "content": "Classify: I was charged twice for April"}]
      }
    },
    {
      "custom_id": "ticket-2", 
      "params": {
        "model": "claude-sonnet-4-6",
        "messages": [{"role": "user",
                      "content": "Classify: API returning 429 error"}]
      }
    },
    ...10,000 entries total
  ]
}

Each entry is a fully self-contained Claude request — its own model, its own messages, its own parameters.


What Anthropic processes asynchronously:

Your submission (1 HTTP call):
"Here are 10,000 independent Claude requests"

Anthropic internally:
Request 1  → spins up Claude → classifies ticket-1  → stores result
Request 2  → spins up Claude → classifies ticket-2  → stores result
Request 3  → spins up Claude → classifies ticket-3  → stores result
...
(spread across their infrastructure, parallel, over hours)

Your polling call (hours later):
"Is batch_id_123 complete?"
→ YES → fetch 10,000 individual results


We get one single batch_id for the entire submission. Not 10,000 batch IDs.


What Anthropic returns for your entire submission:

{
  "id":         "batch_01ABC",    ← one single batch_id for all 10,000 requests
  "status":     "in_progress",
  "request_counts": {
    "total":      10000,
    "processing": 10000,
    "succeeded":  0,
    "errored":    0
  },
  "created_at":  1234567890,
  "expires_at":  1234654290
}

One batch_id covers all 10,000 requests inside it.


Your polling loop — checking one batch_id only:

String batchId = "batch_01ABC";
// Poll on a schedule — not a tight loop
while (true) {
    BatchStatus status = checkBatchStatus(batchId);
    
    if (status.isComplete()) {
        break;  // all 10,000 done
    }
    
    log.info("Still processing: {}/{} complete", 
             status.getSucceeded(), 
             status.getTotal());
             
    Thread.sleep(Duration.ofMinutes(10));  // check every 10 minutes
}

You poll once against that single batch_id — not 10,000 times.


When complete, you fetch all results in one call:

// One call fetches all 10,000 results
List<BatchResult> results = fetchBatchResults(batchId);

// Match each result back to your input using custom_id
results.forEach(result -> {
    String ticketId    = result.getCustomId();   // "ticket-1", "ticket-2" etc
    String classification = result.getContent(); // "BILLING", "TECHNICAL" etc
    
    updateTicket(ticketId, classification);
});

The complete flow visualised:

Your code                                                      Anthropic
──────────────────                 ──────────────────────────
Submit 10,000 requests  →                      Receives entire batch
                                                             Returns ONE batch_01ABC
                                   
                                                                   Internally processes all
                                                                   10,000 requests async...
                                   
Poll batch_01ABC        →          status: in_progress (3,000/10,000 done)
Poll batch_01ABC        →          status: in_progress (7,500/10,000 done)
Poll batch_01ABC        →          status: complete    (10,000/10,000 done)

Fetch results                  →          Returns all 10,000 results
Match via custom_id                in arbitrary order
Update your database

Bottom line

One submission = one batch_id. You poll that single batch_id on a schedule until Anthropic tells you it's complete. Then you fetch all results in one call and use custom_id to match each result back to its original input. No nested loops, no 10,000 individual status checks.