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:
- A shared memory zone definition
- A location that tracks uploads
- 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:
- The same
X-Progress-ID(or custom header) is used for both upload and progress requests - The upload location has
track_uploadsdirective after the content handler - 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.
