June 4, 2026

Resumable PDF Uploads with tus.io: A Complete Guide

tusuploadresumableapilarge-file

Anyone who has built a PDF upload feature knows the pain: PHP's upload_max_filesize cuts off the request, Nginx returns a 413, or a mobile user's connection drops halfway through a 50 MB file and they have to start all over. These are not edge cases — they are the norm when dealing with large documents.

On May 31, 2026, FUNBREW PDF shipped native support for the tus.io resumable upload protocol. This guide walks through how it works, how to implement it, and what to keep in mind for production deployments.


Why Standard Multipart Uploads Fall Short

Traditional multipart/form-data uploads send the entire file in a single HTTP request. That design works fine for small files, but breaks down quickly at scale.

Problem Symptom
Server-side size limits upload_max_filesize (PHP) or client_max_body_size (Nginx) returns 413
Proxy timeouts Long-running uploads get killed by intermediate proxies
Network interruptions Wi-Fi drops or mobile dead zones mean starting over
No progress feedback Accurate upload progress is hard to surface in the UI

These issues are particularly painful in enterprise workflows where users upload multi-hundred-megabyte PDFs from laptops on hotel Wi-Fi or from mobile devices in areas with spotty coverage.


What Is tus?

tus.io is an open protocol for resumable file uploads. Version 1.0.0 of the spec is stable and widely adopted — GitLab, Vimeo, and Transloadit all use it.

The core idea is straightforward: chunking plus offset tracking.

  1. The client sends a POST to reserve an upload slot; the server issues an upload key.
  2. The client sends file data in chunks via PATCH, each time specifying where it left off (Upload-Offset).
  3. After an interruption, the client sends a HEAD to learn the current offset, then resumes from there.

Because each chunk is a separate HTTP request, PHP's upload_max_filesize is irrelevant — no single request carries the full file. Combined with client_max_body_size 0 in Nginx, you can effectively remove file size limits entirely.


tus in FUNBREW PDF API

Endpoints

FUNBREW PDF implements tus protocol v1.0.0 with the following endpoints:

Method Path Purpose
OPTIONS /api/pdf/uploads Discover server capabilities and supported extensions
POST /api/pdf/uploads Reserve an upload slot; returns Location with the upload key
HEAD /api/pdf/uploads/{key} Get current Upload-Offset to find the resume point
PATCH /api/pdf/uploads/{key} Write a chunk of data
DELETE /api/pdf/uploads/{key} Cancel and discard an upload

Authentication: Use your existing PDF API key in the X-API-Key header — no additional credentials needed.

Key Headers

Header Direction Description
Tus-Resumable: 1.0.0 Request Required on every tus request
Upload-Length POST request Total file size in bytes
Upload-Offset PATCH request / HEAD response Byte position to start writing / current received position
Upload-Metadata POST request Filename and other metadata (base64-encoded values)
Content-Type: application/offset+octet-stream PATCH request Required for all chunk write requests

Implementation Examples

1. Raw Protocol with curl

Working through tus with curl first is the best way to understand what is actually happening.

export KEY="sk-your-api-key"
export BASE="https://pdf.funbrew.cloud"
export FILE="large-document.pdf"
export FILE_SIZE=$(wc -c < "$FILE")

# Step 1: Reserve an upload slot (POST)
LOCATION=$(curl -s -D - -X POST "$BASE/api/pdf/uploads" \
  -H "X-API-Key: $KEY" \
  -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Length: $FILE_SIZE" \
  -H "Upload-Metadata: filename $(basename $FILE | base64)" \
  | grep -i "^location:" | tr -d '\r' | awk '{print $2}')

echo "Upload URL: $LOCATION"
# → https://pdf.funbrew.cloud/api/pdf/uploads/abc-def-uuid

# Step 2: Send the first chunk (PATCH)
# For clarity, this example sends the entire file in one chunk.
# In production, split into 5 MB pieces.
curl -i -X PATCH "$LOCATION" \
  -H "X-API-Key: $KEY" \
  -H "Tus-Resumable: 1.0.0" \
  -H "Upload-Offset: 0" \
  -H "Content-Type: application/offset+octet-stream" \
  --data-binary @"$FILE"
# → 204 No Content
# Upload-Offset: <total_bytes>

# Step 3: After an interruption, check where we left off (HEAD)
curl -i -X HEAD "$LOCATION" \
  -H "X-API-Key: $KEY" \
  -H "Tus-Resumable: 1.0.0"
# → 200 OK
# Upload-Offset: <bytes_received>
# Upload-Length: <total_bytes>

The offset from HEAD tells you exactly where to start the next PATCH.

2. @funbrew/pdf-client SDK (Recommended)

The FUNBREW PDF JavaScript SDK wraps tus-js-client so you do not need to manage any of the protocol details manually.

<!-- CDN — no bundler required -->
<script src="https://pdf.funbrew.cloud/sdk/pdf-client.umd.js"></script>
import { PdfClient } from 'https://pdf.funbrew.cloud/sdk/pdf-client.esm.js';

const client = new PdfClient({
  baseUrl: 'https://pdf.funbrew.cloud',
  apiKey: 'sk-your-api-key',
});

// Upload with automatic resume and retry
const result = await client.uploadFile(file, {
  chunkSize: 5 * 1024 * 1024,  // 5 MB chunks
  onProgress: (sent, total) => {
    const pct = ((sent / total) * 100).toFixed(1);
    progressBar.style.width = `${pct}%`;
    label.textContent = `${pct}%`;
  },
});

console.log(result.key);  // UUID — use this as upload_id in subsequent API calls

// Pass directly to a PDF processing endpoint
const compressed = await client.compress({
  upload_id: result.key,
  quality: 'medium',
});
console.log(compressed.download_url);

TypeScript type definitions are available at https://pdf.funbrew.cloud/sdk/types/index.d.ts.

3. tus-js-client (Direct)

For more control over the upload behavior, use the official tus client directly.

npm install tus-js-client
import * as tus from 'tus-js-client';

interface UploadOptions {
  file: File;
  apiKey: string;
  onProgress?: (percentage: number) => void;
  onSuccess?: (uploadUrl: string) => void;
  onError?: (error: Error) => void;
}

function startResumableUpload({
  file,
  apiKey,
  onProgress,
  onSuccess,
  onError,
}: UploadOptions): tus.Upload {
  const upload = new tus.Upload(file, {
    endpoint: 'https://pdf.funbrew.cloud/api/pdf/uploads',
    headers: { 'X-API-Key': apiKey },

    // 5 MB is a good default; drop to 1–2 MB for mobile or unstable connections
    chunkSize: 5 * 1024 * 1024,

    // Exponential back-off retry delays (milliseconds)
    retryDelays: [0, 1000, 3000, 5000, 10000],

    metadata: {
      filename: file.name,
      filetype: file.type,
    },

    onProgress: (bytesUploaded, bytesTotal) => {
      const pct = (bytesUploaded / bytesTotal) * 100;
      onProgress?.(pct);
    },

    onSuccess: () => {
      // upload.url holds the completed upload URL
      // The trailing UUID segment is the upload key
      onSuccess?.(upload.url ?? '');
    },

    onError: (error) => {
      onError?.(error as Error);
    },
  });

  // Check localStorage for a previous upload fingerprint and resume if found
  upload.findPreviousUploads().then((previousUploads) => {
    if (previousUploads.length > 0) {
      upload.resumeFromPreviousUpload(previousUploads[0]);
    }
    upload.start();
  });

  return upload;

  // To cancel: upload.abort();
}

findPreviousUploads() stores a fingerprint in localStorage. If the user reloads the page mid-upload, the SDK will automatically resume from the last confirmed offset.


Resume Flow Diagram

Here is what happens during a network interruption:

Client                            Server
  |                                  |
  |-- POST /api/pdf/uploads ------→ |  Reserve upload slot
  |←-- 201, Location: /..../abc ---|  Upload key issued
  |                                  |
  |-- PATCH (offset=0, 5 MB) ----→ |  Send chunk 1
  |←-- 204, Upload-Offset: 5 MB --|
  |                                  |
  |-- PATCH (offset=5 MB, 5 MB) -→ |  Send chunk 2
  ~~~  Network drops  ~~~
  |                                  |
  |  (Connection restored)           |
  |-- HEAD /..../abc -----------→  |  Where did we stop?
  |←-- 200, Upload-Offset: 8 MB --|  8 MB confirmed received
  |                                  |
  |-- PATCH (offset=8 MB, ...) --→ |  Resume from 8 MB
  |←-- 204, Upload-Offset: total--|  Complete

The server always records how many bytes it has confirmed writing. A HEAD request gives the client that exact number, so there is never any ambiguity about where to resume.


Parallel Uploads (Concatenation Extension)

For very large files or high-latency connections, the Concatenation extension can significantly improve throughput. The client splits the file into N parts, uploads them in parallel, and the server stitches them together.

// tus-js-client: just add parallelUploads
const upload = new tus.Upload(file, {
  endpoint: 'https://pdf.funbrew.cloud/api/pdf/uploads',
  parallelUploads: 4,
  headers: { 'X-API-Key': apiKey },
});
upload.start();

The same option works through the SDK:

const result = await client.uploadFile(file, {
  parallelUploads: 4,
  onProgress: (sent, total) => console.log(`${sent}/${total}`),
});
// result.key is the final (concatenated) upload key

Limitation: Concatenation is not supported when PDF_TUS_STORAGE=s3-multipart is enabled on the server. Requests with Upload-Concat headers will receive a 400 in that mode.


Webhook Notifications

To receive a server-side push when an upload finishes, enable webhook events in your dashboard.

Add tus.upload.completed to your webhook configuration and you will receive:

{
  "event": "tus.upload.completed",
  "timestamp": "2026-06-04T09:00:00.000Z",
  "data": {
    "upload_key": "abc-def-uuid",
    "original_filename": "quarterly-report.pdf",
    "size": 15728640,
    "storage": "local",
    "s3_key": null,
    "content_type": "application/pdf"
  }
}
Event Fires when
tus.upload.completed Final chunk received and the upload is complete
tus.upload.aborted DELETE received, or the GC cleans up an expired upload

These events use the same signing, retry, and delivery-log infrastructure as pdf.generated events. See the PDF API Webhook Integration Guide for full details.


Piping Uploads Into PDF Processing

Once an upload is complete, pass its key directly to any PDF processing endpoint. No intermediate download and re-upload step is needed.

# Upload → merge two PDFs
curl -X POST https://pdf.funbrew.cloud/api/pdf/merge \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "upload_ids": ["abc-def-uuid", "xyz-uvw-uuid"],
    "expiration_hours": 24
  }'

# Upload → compress
curl -X POST https://pdf.funbrew.cloud/api/pdf/compress \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"upload_id": "abc-def-uuid", "quality": "medium"}'

Supported endpoints: merge, compress, extract-text, to-image. Passing an incomplete upload key returns 422.

Each processing endpoint responds with an X-Job-Id header you can use to subscribe to SSE progress events (GET /api/pdf/jobs/{jobId}/events), which we cover in the SSE Progress Streaming Guide.


Production Considerations

Choosing a Chunk Size

Environment Recommended chunk size
Fast fixed broadband 10–20 MB
Typical office Wi-Fi 5 MB (default)
Mobile or unstable connections 1–2 MB

Smaller chunks reduce the amount of data that needs to be re-sent after an interruption, but add per-request overhead. Tune based on your users' connection profiles.

Garbage Collection

Abandoned uploads are cleaned up automatically. The tus-php cache TTL expires entries after one day, and the pdf:cleanup-tus-uploads artisan command runs daily at 03:25 to remove orphaned files.

As a best practice, pass the upload key to a processing endpoint as soon as the upload completes — well within the 24-hour window.

Access Control

Each upload key is scoped to the company that created it. HEAD, PATCH, and DELETE requests from a different company return 404. Keys are UUIDs and are not guessable, but follow the API key management practices in the PDF API Production Guide for full security hygiene.

Nginx Configuration

If you are proxying tus requests through Nginx, make sure your config does not buffer request bodies or impose its own size limit:

location /api/pdf/uploads {
    client_max_body_size 0;       # No limit — tus controls chunk size
    client_body_buffer_size 1m;
    proxy_request_buffering off;  # Stream chunks immediately; do not buffer
    proxy_read_timeout 3600s;     # Allow time for large chunk transfers
}

Summary

tus.io resumable uploads solve a class of problems that standard multipart uploads simply cannot handle gracefully.

  • File size limits (upload_max_filesize, client_max_body_size) stop being a constraint
  • Network interruptions no longer mean starting over
  • Chunk-level progress is easy to surface in the UI
  • Completed uploads pipe directly into PDF processing endpoints

FUNBREW PDF supports uploads up to 5 GB. Try the full upload-to-processing flow in the Playground, browse the API documentation, or see real-world patterns in the use cases section.

For deeper coverage of the JavaScript SDK — including PDF generation, SSE progress subscriptions, and the full TypeScript API surface — see the JS SDK guide.


Related Articles

Powered by FUNBREW PDF