Skip to main content

NGINX

NGINX Upload Progress Module: Real-Time File Upload Tracking

by , , revisited on


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

The NGINX upload progress module enables real-time monitoring of file uploads, allowing you to display progress bars and upload statistics to users. This module works seamlessly with both standard PHP uploads and the high-performance NGINX upload module, providing a complete solution for handling large file uploads in web applications.

How NGINX Upload Progress Tracking Works

The NGINX upload progress module uses shared memory zones to track upload state across multiple worker processes. When a client initiates an upload with a unique tracking ID, NGINX stores the upload progress in shared memory. A separate endpoint can then query this shared memory to retrieve real-time progress data.

The module tracks four upload states:

  • starting – Upload registered but data transfer hasn’t begun
  • uploading – Data is being received, with bytes received and total size available
  • done – Upload completed successfully
  • error – Upload failed with an HTTP error status

This architecture enables AJAX-based progress polling without blocking the main upload connection. The result is a responsive user experience even for large file transfers.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux

Install the NGINX upload progress module from the GetPageSpeed repository:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-upload-progress

Then load the module in your NGINX configuration:

load_module modules/ngx_http_uploadprogress_module.so;

For the complete upload solution with efficient file handling, also install the upload module:

sudo dnf install nginx-module-upload

And load it alongside the progress module:

load_module modules/ngx_http_upload_module.so;
load_module modules/ngx_http_uploadprogress_module.so;

Debian and Ubuntu

First, set up the GetPageSpeed APT repository, then install:

sudo apt-get update
sudo apt-get install nginx-module-upload-progress

On Debian/Ubuntu, the package handles module loading automatically. No load_module directive is needed.

View the full module documentation at:
– RPM: nginx-extras.getpagespeed.com/modules/upload-progress/
– APT: apt-nginx-extras.getpagespeed.com/modules/upload-progress/

Basic Configuration

The NGINX upload progress module requires three components:

  1. A shared memory zone definition
  2. A location that tracks uploads
  3. A location that reports progress

Minimal Working Configuration

http {
    # Create shared memory zone for progress tracking
    upload_progress uploads 1m;

    server {
        listen 80;
        client_max_body_size 100m;

        # Progress reporting endpoint
        location ^~ /progress {
            report_uploads uploads;
            upload_progress_json_output;
        }

        # Upload endpoint with tracking
        location /upload {
            fastcgi_pass unix:/run/php-fpm/www.sock;
            fastcgi_param SCRIPT_FILENAME /var/www/html/upload.php;
            include fastcgi_params;
            track_uploads uploads 30s;
        }
    }
}

The ^~ prefix on the progress location ensures it takes priority over other location matches.

Configuration Directives

upload_progress

Syntax: upload_progress zone_name size;
Context: http
Default: none

Creates a shared memory zone for storing NGINX upload progress data. The zone name is referenced by track_uploads and report_uploads directives. Size determines how many concurrent uploads can be tracked.

upload_progress uploads 1m;

A 1MB zone typically supports hundreds of concurrent tracked uploads.

track_uploads

Syntax: track_uploads zone_name timeout;
Context: http, server, location
Default: none

Enables upload tracking for a location. This directive must appear after the content handler directive (fastcgi_pass, proxy_pass, or upload_pass). The timeout specifies how long to retain tracking data after upload completion.

location /upload {
    fastcgi_pass unix:/run/php-fpm/www.sock;
    include fastcgi_params;
    track_uploads uploads 60s;
}

report_uploads

Syntax: report_uploads zone_name;
Context: http, server, location
Default: none

Creates an endpoint that returns NGINX upload progress data. Clients poll this endpoint to retrieve real-time progress information.

location /progress {
    report_uploads uploads;
}

upload_progress_json_output

Syntax: upload_progress_json_output;
Context: http, server, location
Default: JSONP output

Configures the progress endpoint to return pure JSON responses with application/json content type.

Output format:

{ "state" : "uploading", "received" : 1048576, "size" : 5242880 }

upload_progress_jsonp_output

Syntax: upload_progress_jsonp_output;
Context: http, server, location
Default: enabled

Configures JSONP output for cross-domain progress polling. The callback function name is read from a query parameter.

upload_progress_java_output

Syntax: upload_progress_java_output;
Context: http, server, location
Default: none

Configures JavaScript object notation output compatible with legacy applications.

upload_progress_content_type

Syntax: upload_progress_content_type type;
Context: http, server, location
Default: text/javascript

Sets the Content-Type header for progress responses.

upload_progress_content_type application/json;

upload_progress_header

Syntax: upload_progress_header header_name;
Context: http, server, location
Default: X-Progress-ID

Specifies the HTTP header or query parameter name used to identify uploads. Clients must include this identifier in both upload and progress check requests.

upload_progress_header X-Upload-ID;

upload_progress_jsonp_parameter

Syntax: upload_progress_jsonp_parameter param_name;
Context: http, server, location
Default: callback

Specifies the query parameter name for the JSONP callback function.

upload_progress_template

Syntax: upload_progress_template state template;
Context: http, server, location
Default: built-in templates

Customizes the output format for a specific upload state. Available states: starting, uploading, done, error.

upload_progress_template uploading "Progress: $uploadprogress_received of $uploadprogress_length bytes";

Available Variables

The NGINX upload progress module provides these variables for use in templates and logging:

Variable Description
$uploadprogress_received Bytes received so far
$uploadprogress_remaining Bytes remaining to transfer
$uploadprogress_length Total upload size in bytes
$uploadprogress_status HTTP error status code (error state only)
$uploadprogress_callback JSONP callback function name

Complete Example with PHP

This example demonstrates a complete upload system with real-time NGINX upload progress tracking.

NGINX Configuration

load_module modules/ngx_http_uploadprogress_module.so;

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    upload_progress uploads 1m;

    server {
        listen 80;
        server_name localhost;
        root /var/www/html;
        client_max_body_size 100m;

        # Progress endpoint
        location ^~ /progress {
            report_uploads uploads;
            upload_progress_json_output;
        }

        # PHP upload handler
        location = /upload.php {
            fastcgi_pass unix:/run/php-fpm/www.sock;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include fastcgi_params;
            track_uploads uploads 30s;
        }

        location / {
            index index.html;
        }
    }
}

PHP Upload Handler

Create /var/www/html/upload.php:

<?php
header('Content-Type: application/json');

$response = ['success' => false, 'message' => ''];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
        $uploadDir = '/var/www/uploads/';
        if (!is_dir($uploadDir)) {
            mkdir($uploadDir, 0755, true);
        }

        $filename = basename($_FILES['file']['name']);
        $destPath = $uploadDir . $filename;

        if (move_uploaded_file($_FILES['file']['tmp_name'], $destPath)) {
            $response['success'] = true;
            $response['message'] = 'File uploaded successfully';
            $response['file'] = [
                'name' => $filename,
                'size' => $_FILES['file']['size']
            ];
        } else {
            $response['message'] = 'Failed to move uploaded file';
        }
    } else {
        $response['message'] = 'No file uploaded or upload error';
    }
}

echo json_encode($response);

HTML Upload Form with Progress Bar

Create /var/www/html/index.html. The form submits to an iframe to allow progress polling without interrupting the page:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Upload with Progress</title>
    <style>
        .progress-bar {
            width: 100%;
            height: 30px;
            background: #ddd;
            border-radius: 5px;
            overflow: hidden;
            margin: 20px 0;
        }
        .progress-fill {
            height: 100%;
            background: #4CAF50;
            width: 0%;
            transition: width 0.3s;
        }
        .progress-text {
            text-align: center;
            margin-top: 10px;
        }
    </style>
</head>
<body>
    <h1>File Upload with Progress</h1>

    <form id="uploadForm" action="/upload.php" method="POST"
          enctype="multipart/form-data" target="upload_frame">
        <input type="file" name="file" id="fileInput" required>
        <button type="submit">Upload</button>
    </form>

    <div class="progress-bar">
        <div class="progress-fill" id="progressFill"></div>
    </div>
    <div class="progress-text" id="progressText">Select a file to upload</div>

    <iframe name="upload_frame" style="display:none;"></iframe>

    <script>
    function generateUUID() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
            var r = Math.random() * 16 | 0;
            return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
        });
    }

    document.getElementById('uploadForm').addEventListener('submit', function(e) {
        var uuid = generateUUID();
        this.action = '/upload.php?X-Progress-ID=' + uuid;

        var progressFill = document.getElementById('progressFill');
        var progressText = document.getElementById('progressText');

        progressFill.style.width = '0%';
        progressText.textContent = 'Starting upload...';

        var interval = setInterval(function() {
            fetch('/progress?X-Progress-ID=' + uuid)
                .then(response => response.json())
                .then(data => {
                    if (data.state === 'uploading') {
                        var percent = Math.round((data.received / data.size) * 100);
                        progressFill.style.width = percent + '%';
                        progressText.textContent = percent + '% (' +
                            formatBytes(data.received) + ' / ' + formatBytes(data.size) + ')';
                    } else if (data.state === 'done') {
                        progressFill.style.width = '100%';
                        progressText.textContent = 'Upload complete!';
                        clearInterval(interval);
                    } else if (data.state === 'error') {
                        progressText.textContent = 'Upload failed: ' + data.status;
                        clearInterval(interval);
                    }
                });
        }, 200);
    });

    function formatBytes(bytes) {
        if (bytes === 0) return '0 Bytes';
        var k = 1024;
        var sizes = ['Bytes', 'KB', 'MB', 'GB'];
        var i = Math.floor(Math.log(bytes) / Math.log(k));
        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    }
    </script>
</body>
</html>

Integration with NGINX Upload Module

For high-performance file uploads that bypass PHP’s memory limitations, combine the NGINX upload progress module with the NGINX upload module. This combination provides real-time progress tracking and direct file storage without PHP memory overhead. You also get automatic cleanup of failed uploads and file metadata extraction including size and MD5 hash.

Combined Configuration

load_module modules/ngx_http_upload_module.so;
load_module modules/ngx_http_uploadprogress_module.so;

http {
    upload_progress uploads 1m;

    server {
        listen 80;
        client_max_body_size 1g;

        location ^~ /progress {
            report_uploads uploads;
            upload_progress_json_output;
        }

        location /upload {
            upload_pass @upload_handler;
            upload_store /var/www/uploads 1;
            upload_store_access user:rw group:rw all:r;

            upload_set_form_field $upload_field_name.name "$upload_file_name";
            upload_set_form_field $upload_field_name.path "$upload_tmp_path";
            upload_aggregate_form_field $upload_field_name.size "$upload_file_size";
            upload_aggregate_form_field $upload_field_name.md5 "$upload_file_md5";

            upload_pass_form_field ".*";
            upload_cleanup 400 404 499 500-505;

            track_uploads uploads 60s;
        }

        location @upload_handler {
            fastcgi_pass unix:/run/php-fpm/www.sock;
            fastcgi_param SCRIPT_FILENAME /var/www/html/handle_upload.php;
            include fastcgi_params;
        }
    }
}

The track_uploads directive must appear after upload_pass to correctly wrap the upload handler.

Performance Considerations

Shared Memory Sizing

The shared memory zone stores tracking data for concurrent uploads. Each tracked upload requires approximately 128 bytes plus the length of the tracking ID. A 1MB zone supports roughly 8,000 concurrent tracked uploads.

# Small deployments (few concurrent uploads)
upload_progress uploads 512k;

# Large deployments (many concurrent uploads)
upload_progress uploads 4m;

Polling Frequency

Client-side polling frequency affects both user experience and server load:

  • 100-200ms – Smooth progress bar updates, higher server load
  • 500ms – Good balance for most applications
  • 1000ms+ – Lower server load, less responsive UI

Cleanup Timeout

The track_uploads timeout controls how long completed upload data remains in shared memory. Set this long enough for the final progress check to succeed:

# Short timeout for high-traffic servers
track_uploads uploads 15s;

# Longer timeout for debugging
track_uploads uploads 120s;

Security Best Practices

Limit Progress Access

Restrict NGINX upload progress endpoint access to prevent information disclosure:

location ^~ /progress {
    # Only allow same-origin requests
    if ($http_referer !~* ^https?://yourdomain\.com/) {
        return 403;
    }
    report_uploads uploads;
    upload_progress_json_output;
}

Validate Tracking IDs

Use the upload_progress_header directive with a non-standard header name to obscure the tracking mechanism:

upload_progress_header X-Upload-Token;

Rate Limiting

Apply rate limiting to the progress endpoint to prevent abuse:

limit_req_zone $binary_remote_addr zone=progress:10m rate=10r/s;

location ^~ /progress {
    limit_req zone=progress burst=20;
    report_uploads uploads;
    upload_progress_json_output;
}

Troubleshooting

Progress Always Shows “starting”

This typically indicates the tracking ID isn’t being found. Verify:

  1. The same X-Progress-ID (or custom header) is used for both upload and progress requests
  2. The upload location has track_uploads directive after the content handler
  3. Both locations reference the same shared memory zone

500 Error on Upload

A 500 error with “tracking already registered id” in the error log means a duplicate tracking ID was used. Ensure clients generate unique IDs for each upload:

// Generate unique UUID for each upload
var uuid = crypto.randomUUID();

Progress Data Missing After Restart

NGINX upload progress data is stored in shared memory and doesn’t survive NGINX restarts. This is by design. Completed uploads should be verified through your application’s database, not the progress module.

HTTP/2 Considerations

The NGINX upload progress module supports HTTP/2 uploads. Progress tracking works correctly because NGINX handles the HTTP/2 to HTTP/1.1 translation internally.

Conclusion

The NGINX upload progress module provides essential real-time feedback for file uploads, significantly improving user experience for web applications that handle large files. Combined with the NGINX upload module, it creates a high-performance upload system that scales to handle files of any size.

For the source code and issue tracking, visit the nginx-upload-progress-module GitHub repository.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.