The most surprising thing about handling file uploads in a monolith is how often they become the unsung heroes of performance bottlenecks, even when everything else seems fine.
Imagine a typical web application. A user clicks "upload," their browser sends a multipart/form-data POST request, and the monolith’s web server, say Nginx, receives it. Nginx then passes this request to the application server, perhaps a Python/Gunicorn or Ruby/Puma setup. Inside the application, frameworks like Django or Rails intercept the request, parse the file data, and then, based on configuration, decide where to put it. This could be on local disk, a network attached storage (NAS), or a cloud object store like AWS S3.
Let’s see this in action with a simplified Python Flask example. A user uploads a file through a form:
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="Upload">
</form>
The Flask app handles it:
from flask import Flask, request, redirect, url_for
import os
UPLOAD_FOLDER = '/app/uploads'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
if 'file' not in request.files:
return 'No file part'
file = request.files['file']
if file.filename == '':
return 'No selected file'
if file and allowed_file(file.filename):
filename = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
file.save(filename)
return redirect(url_for('uploaded_file', filename=filename))
return '''
<!doctype html>
<title>Upload new File</title>
<h1>Upload new File</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>
</form>
'''
if __name__ == '__main__':
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
app.run(debug=True)
The mental model here is straightforward: request comes in, file is extracted, saved. The problem arises when "saved" means writing to disk. The monolith’s application server process is typically CPU-bound and has limited I/O capacity. When many users upload large files concurrently, the disk I/O becomes saturated. The application server gets bogged down waiting for disk writes to complete, blocking other requests and slowing down the entire monolith. This isn’t just about disk speed; it’s about the application process itself being tied up by synchronous I/O operations.
The levers you control are primarily the storage backend and how the application interacts with it.
- Storage Location: Is it local disk on the app server? A shared network drive (NAS)? An object store (S3, GCS)?
- Application Configuration: How is the
UPLOAD_FOLDERconfigured? Is the application itself doing the writing, or is it offloading to a dedicated service? - Web Server Configuration: Nginx or Apache can be configured to proxy uploads directly to storage, bypassing the application server for the heavy lifting.
The one thing most people don’t realize is that even when using a fast SSD on the application server, a high volume of concurrent file uploads can still exhaust the available IOPS (Input/Output Operations Per Second) and create a bottleneck within the application process itself. The process isn’t just waiting for the disk; it’s actively managing the data buffer, performing checks, and then handing it off, all of which consumes CPU cycles that could be used for other tasks. If your application server is already serving API requests or rendering HTML, these blocking I/O operations can have a cascading effect.
The next problem you’ll run into is managing and serving these stored files efficiently, especially at scale.