The File Upload Trap
You asked Cursor to add a file upload feature to your app. It generated an endpoint that accepts a file, saves it to disk, and returns the URL. Locally, it works perfectly: you upload a 5MB image, it saves to uploads/, and the image displays. You deploy to production, and the first user uploads a 50MB video. The container runs out of memory and crashes. Or the upload takes 2 minutes and times out. Or the file is saved to the container's ephemeral filesystem and disappears when the container restarts. This is the file upload trap, and it is one of the most common failure modes for AI-generated apps. Here are the 6 reasons AI-generated file upload endpoints break in production, and the production checklist to fix each one.
The direct answer is that file uploads are a deceptively complex feature that AI assistants get wrong in predictable ways. The 6 reasons are: in-memory buffering, request timeouts, missing validation, security vulnerabilities, ephemeral storage, and missing cleanup. Each one has a known cause and a known fix, and applying all 6 fixes gives you a production-ready file upload system. For more on why AI apps break under real conditions, see our article on why AI apps break on the first real user.
Reason 1: In-Memory Buffering
The most common reason AI-generated file upload endpoints break is in-memory buffering. AI assistants often use libraries like multer (for Express) or FastAPI's default UploadFile that buffer the entire file in memory before saving it to disk. This works for small files (e.g., a 1MB profile photo), but for large files (e.g., a 50MB video), the memory usage spikes and can crash the container. The fix is to stream files to disk (or external storage) instead of buffering them in memory. For Express, use multer with diskStorage instead of memoryStorage. For FastAPI, use UploadFile with a custom SpooledTemporaryFile that spills to disk for large files. The key insight is that file uploads should never consume more than a few megabytes of memory, regardless of file size.
Reason 2: Request Timeouts
The second reason is request timeouts. Large file uploads take time, especially on slow network connections. A 50MB file on a 1Mbps connection takes 400 seconds to upload, which exceeds most request timeouts (e.g., Vercel's 10-second timeout, Deployxa's default 300-second timeout). The fix is to increase the request timeout for upload endpoints and to use chunked uploads for very large files. For Express, set server.timeout = 0 (no timeout) or a high value for upload routes. For Deployxa, the default timeout is 300 seconds, which is enough for most uploads. For files larger than 100MB, use chunked uploads (the client splits the file into chunks and uploads them separately, then the server reassembles them).
Reason 3: Missing Validation
The third reason is missing validation. AI assistants often accept any file without validating its type, size, or content. This is a security risk, because an attacker can upload a malicious file (e.g., a PHP script disguised as an image) and execute it on the server. The fix is to validate files on three levels: file type (check the MIME type and extension), file size (set a maximum size), and file content (for images, use a library like sharp to verify the image is valid). For Express, use multer's fileFilter option. For FastAPI, use a custom validator. Never trust the client-provided MIME type; always verify it on the server.
Reason 4: Security Vulnerabilities
The fourth reason is security vulnerabilities. AI assistants often save uploaded files with their original filenames, which can contain path traversal characters (e.g., ../../../etc/passwd) that allow the attacker to overwrite system files. They also often serve uploaded files from the same origin as the app, which can lead to XSS attacks (if the uploaded file is HTML or JavaScript). The fix is to: generate a random filename for each upload (e.g., a UUID), save files outside the web root (or in a dedicated storage bucket), and serve files from a different origin (or with Content-Disposition: attachment to prevent inline rendering). For more on security, see our article on the JWT authentication trap.
Reason 5: Ephemeral Storage
The fifth reason is ephemeral storage. AI assistants often save uploaded files to the container's local filesystem (e.g., uploads/), which works locally but fails in production, because containers have ephemeral filesystems. When the container restarts (e.g., for a blue/green deployment), the files are lost. The fix is to save files to persistent storage: either an external object storage service (e.g., AWS S3, Cloudflare R2) or a persistent volume mounted to the container. For most apps, external object storage is the better choice, because it is scalable, durable, and accessible from multiple containers. For Deployxa, you can provision an S3-compatible bucket from Cloudflare R2, AWS S3, or Backblaze B2, and store the credentials as environment variables.
Reason 6: Missing Cleanup
The sixth reason is missing cleanup. AI assistants rarely add cleanup logic for uploaded files, which means files accumulate over time and fill up the storage. This is especially problematic for temporary files (e.g., a resized image that was generated as part of a processing pipeline). The fix is to implement a cleanup strategy: delete temporary files after they are used, delete orphaned files (files that are not associated with any database record) periodically, and set up lifecycle policies on your storage bucket (e.g., delete files older than 30 days). For Express, use a cron job (via node-cron) to clean up temporary files. For more on cron jobs, see our article on why AI-generated cron jobs don't run on serverless.
Step-by-Step: Implementing a Production-Ready File Upload
Here is the exact workflow for implementing a production-ready file upload in a typical Express app.
Step 1: Install dependencies
npm install multer @aws-sdk/client-s3 sharpStep 2: Configure multer for disk storage
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');
const storage = multer.diskStorage({
destination: '/tmp/uploads/',
filename: (req, file, cb) => {
// Generate a random filename to prevent path traversal
const ext = path.extname(file.originalname);
const name = crypto.randomUUID() + ext;
cb(null, name);
},
});
const upload = multer({
storage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
fileFilter: (req, file, cb) => {
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'), false);
}
},
});Step 3: Configure S3 client
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const s3 = new S3Client({
region: process.env.S3_REGION,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY,
},
});
async function uploadToS3(filePath, key) {
const fs = require('fs');
const fileContent = fs.readFileSync(filePath);
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: fileContent,
ContentType: 'image/jpeg',
}));
return `https://${process.env.S3_BUCKET}.s3.${process.env.S3_REGION}.amazonaws.com/${key}`;
}Step 4: Implement the upload endpoint
app.post('/upload', upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Verify the image is valid (using sharp)
const sharp = require('sharp');
await sharp(req.file.path).metadata();
// Upload to S3
const url = await uploadToS3(req.file.path, req.file.filename);
// Clean up the temporary file
const fs = require('fs');
fs.unlinkSync(req.file.path);
// Save to database
const file = await File.create({
url,
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
userId: req.user.id,
});
res.json({ url: file.url, id: file.id });
} catch (err) {
res.status(500).json({ error: 'Upload failed' });
}
});Step 5: Set environment variables
In the Deployxa dashboard, set:
- S3_REGION: your S3 region
- S3_ACCESS_KEY: your S3 access key
- S3_SECRET_KEY: your S3 secret key
- S3_BUCKET: your S3 bucket name
The pre-flight scanner will warn you if any are missing.
Step 6: Implement cleanup
const cron = require('node-cron');
// Clean up orphaned files every day at 3 AM
cron.schedule('0 3 * * *', async () => {
const orphanedFiles = await File.findOrphaned();
for (const file of orphanedFiles) {
await s3.send(new DeleteObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: file.filename,
}));
await file.delete();
}
});Step 7: Verify with deployxa doctor
Run deployxa doctor to verify your app's health. The 14-point readiness engine checks SSL, DNS, environment variables, health endpoints, and container status.
Common Pitfalls and Troubleshooting
The first pitfall is accepting any file type. AI assistants often accept any file without validation, which is a security risk. The fix is to validate file type, size, and content. The second pitfall is using the original filename. Original filenames can contain path traversal characters, which is a security risk. The fix is to generate a random filename for each upload. The third pitfall is saving to the local filesystem. Local filesystems are ephemeral, which means files are lost when the container restarts. The fix is to save to external storage (S3, R2). The fourth pitfall is not cleaning up temporary files. Temporary files accumulate over time and fill up the storage. The fix is to clean up temporary files after use and to implement a periodic cleanup job. The fifth pitfall is not handling upload errors. Uploads can fail for many reasons (network error, disk full, S3 error), and you need to handle these errors gracefully. The fix is to wrap the upload logic in a try-catch block and to return a clear error message to the user.
Production Hardening for File Uploads
Beyond the 6 fixes, file upload systems benefit from several additional hardening steps. The first is virus scanning. Uploaded files can contain malware, which can infect other users if served. The fix is to scan uploaded files with an antivirus (e.g., ClamAV) before serving them. The second is image optimization. Uploaded images can be large (e.g., 10MB photos from a phone), which degrades performance. The fix is to optimize images on upload (e.g., resize, convert to WebP) using a library like sharp. The third is content delivery network (CDN) serving. Serving files from your app's container consumes bandwidth and CPU, which can degrade performance. The fix is to serve files from a CDN (e.g., Cloudflare, CloudFront) that caches files at edge locations. The fourth is access control. Not all files should be public; some should only be accessible to specific users. The fix is to use signed URLs (e.g., S3 signed URLs) that expire after a short period, which prevents unauthorized access. The fifth is upload progress. Large file uploads can take a long time, and users want to see progress. The fix is to use XMLHttpRequest or fetch with a progress handler to show upload progress to the user. For more on production hardening, see our articles on the JWT authentication trap and AI error handling failures.
When File Uploads Are Not Needed
Not every app needs file uploads. Apps that do not handle user-generated content (e.g., a calculator, a todo list) do not need file uploads. Apps that use external services for content (e.g., a YouTube embed, a Spotify player) do not need file uploads. For these apps, skipping file upload functionality simplifies the app and reduces the attack surface. The key is to know your users: if your users need to upload files, implement file uploads securely; if they do not, skip it. For more on production patterns, see our articles on the CORS trap and why AI apps break on the first real user.
Conclusion: File Uploads Are Deceptively Complex
The file upload trap is not a sign that your AI assistant did a bad job. It is a sign that file uploads are deceptively complex, and AI assistants generate code that works locally but fails in production. By applying the 6 fixes above (stream to disk, increase timeouts, validate files, secure filenames, use external storage, clean up), you can build a production-ready file upload system that handles real users safely.
Ready to deploy a production-ready file upload? Drag your project to Deployxa Drop for an instant live preview, or install the CLI with npm i -g @deployxa/cli and deploy from your terminal. For more on AI coding patterns, see our articles on the JWT authentication trap and why AI apps break on mobile. Learn about AI error handling failures and the rate limiting gap in our companion articles. Explore our free developer tools to speed up your workflow.