Skip to main content

Architecting Large File Uploads to S3 with Next.js Presigned URLs

NR Tech Studio Team
NR Tech Studio Team NR Tech Studio
11 min read

Handling large file uploads in a web application is a classic architectural bottleneck. When developers attempt to pipe multi-gigabyte files through a Next.js API route or Server Action, they quickly encounter severe infrastructure limitations. The default request body size limits of serverless environments, combined with memory overhead and the inherent latency of proxying data through your application server, make standard multipart uploads unsustainable for production-grade systems.

The solution lies in shifting the data transfer burden away from your compute layer entirely. By utilizing AWS S3 presigned URLs, you authorize the client to communicate directly with your storage bucket, effectively bypassing your Next.js runtime. This approach transforms a resource-heavy server task into a client-side network operation, providing superior scalability and drastically reducing operational costs associated with compute-time consumption.

The Inefficiency of Server-Proxied Uploads

When a developer begins building an upload feature, the most intuitive path is often to receive a multipart/form-data request in an API route. In a Next.js environment, this immediately triggers a series of failure points. First, the bodyParser configuration in standard Node.js environments has strict limits. Even when extended, streaming large files into a buffer consumes memory exponentially, leading to OOM (Out of Memory) errors during concurrency spikes. If you are deploying on Vercel or similar serverless platforms, your execution time is strictly capped; a slow user connection uploading a large file will exceed the function timeout long before the upload completes.

Furthermore, the overhead of proxying data is significant. Your server must receive the byte stream from the user and then forward it to S3. This doubles the network traffic ingress and egress costs for your infrastructure. More importantly, it ties up your compute resources. If you have ten users uploading 500MB files simultaneously, your server is effectively blocked, unable to process other concurrent requests. This architecture is inherently non-scalable and creates a single point of failure that is highly sensitive to network fluctuations between your application server and the AWS data center.

Conceptualizing the Presigned URL Workflow

The presigned URL pattern shifts the architectural responsibility. Instead of the server performing the upload, the server acts solely as an authorization authority. The workflow follows a strict sequence: The client requests a unique, temporary URL from your Next.js backend. Your backend, using the AWS SDK, generates an S3 PUT request URL, cryptographically signed with your IAM credentials. This URL is returned to the client, which then performs an HTTP PUT request directly to S3. The client receives the response directly from AWS, and your server remains entirely out of the data path.

This design is highly resilient. Because the S3 endpoint handles the ingestion, you benefit from the global infrastructure of AWS. The security model is robust because the URL is time-limited and scoped strictly to a specific object key. Even if a malicious actor intercepts the URL, the window of exploitation is minimal. As you refine your application, you might find that managing these backend services requires careful configuration, much like when you are Configuring Cursor AI Rules for Next.js 15 Architectures to maintain codebase consistency.

Configuring AWS IAM and S3 Bucket Policies

Before writing code, the infrastructure must be correctly hardened. The IAM user or role utilized by your Next.js application must have the s3:PutObject permission limited to the specific bucket and path prefix. Avoid blanket permissions. The bucket itself should have CORS (Cross-Origin Resource Sharing) policies configured to allow the PUT method from your application’s domain. Without correct CORS headers, the browser will block the cross-origin request to S3, regardless of the validity of your signature.

A typical CORS configuration for an S3 bucket looks like this:

[{"AllowedHeaders": ["*"], "AllowedMethods": ["PUT"], "AllowedOrigins": ["https://yourdomain.com"], "ExposeHeaders": ["ETag"]}]

This allows the browser to perform the PUT operation. Ensure that your bucket is also configured to block public access, as the security of your files should be managed via these temporary URLs rather than public read permissions. By strictly scoping these policies, you prevent unauthorized data exfiltration and ensure that only your authenticated Next.js sessions can trigger the upload process.

Implementing the Backend Signature Generator

On the server side, you need to use the @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner packages. The logic inside your Next.js API route or Server Action should instantiate an S3 client using environment variables for your AWS keys. The getSignedUrl function is the core utility that handles the cryptographic heavy lifting. It requires the S3 command (e.g., PutObjectCommand) and a configuration object specifying the expiration time.

Example of a secure implementation:

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({ region: process.env.AWS_REGION });

export async function generateUploadUrl(key: string, type: string) {
const command = new PutObjectCommand({ Bucket: 'my-bucket', Key: key, ContentType: type });
return await getSignedUrl(s3, command, { expiresIn: 3600 });
}

By returning this URL to the client, you effectively offload the entire upload lifecycle. Note that keeping these secrets out of the client-side bundle is critical. Always perform this operation within a secure Server Action or API Route to ensure the IAM credentials never leak to the browser.

Client-Side Upload Execution

Once the client receives the presigned URL, the upload process is a standard fetch or XMLHttpRequest call. The key is to set the Content-Type header to match exactly what you specified during the signature generation. If the headers do not match, the S3 server will reject the request with a 403 Forbidden error. Using the File API in the browser, you can stream the file content directly to the URL.

It is best practice to track upload progress using the onUploadProgress handler if using libraries like Axios, or by attaching a listener to the XMLHttpRequest.upload object. This provides a better user experience than a static loading spinner. Remember that since this happens in the client, you can manage retries and error states without impacting your backend server performance. This is particularly useful when Resolving Stripe Webhook Signature Verification Failures in Node.js, as you learn to isolate specific failure domains and handle them gracefully at the integration layer.

Handling Large Files via Multipart Uploads

For files exceeding 5GB, a single PUT request is not sufficient. AWS S3 requires a multipart upload strategy. This involves initiating a multipart upload, uploading chunks individually, and finally completing the upload. While this is more complex to implement, it is mandatory for large assets. You must manage the part numbers and ETags returned by S3 for each chunk to finalize the process correctly. This ensures that if a network connection drops at 80% of a 10GB file, you only need to retry the failed chunk, not the entire file.

You can automate this using the S3 ManagedUpload utility, which abstracts the chunking logic. However, in a custom Next.js implementation, you often need granular control over the progress of each part to provide accurate UI feedback. Always maintain a state machine on the client to track the status of each part. This makes your application resilient to intermittent connectivity, a common challenge in mobile or remote enterprise environments.

Security Considerations and URL Expiration

The security of your system hinges on the expiration time of your presigned URLs. Setting a very long expiration window increases the risk of unauthorized use if the URL is leaked or logged. Conversely, an expiration that is too short might cause issues for users on slow connections. A balance of 30 to 60 minutes is generally sufficient for most web-based file uploads. Always ensure your application logs the creation of these URLs without logging the actual URL string itself, as this would be a security vulnerability.

Additionally, consider adding metadata to your S3 objects. By including specific metadata during the PutObjectCommand, you can tag the file with the user ID or other identifiers. This allows you to perform secondary audits or clean-up tasks using S3 Lifecycle policies. For instance, you could configure a policy to automatically delete incomplete multipart uploads after 24 hours, preventing storage bloat and unnecessary costs.

Monitoring and Observability

Since the data transfer happens outside of your application server, you lose the ability to monitor the upload via server logs. You must implement client-side telemetry to track upload success, failure rates, and latency. Sending these events to a monitoring service like Sentry or Datadog allows you to identify patterns in failed uploads, such as regional network issues or browser-specific incompatibilities. Without this, you are effectively flying blind regarding the most critical path of your data pipeline.

Also, keep an eye on your S3 CloudWatch metrics. Monitoring 4xx and 5xx error rates on your bucket will give you immediate feedback on whether your presigned URLs are being generated correctly or if your CORS policies are misconfigured. This proactive approach to observability is what separates a fragile prototype from a robust, production-ready enterprise application.

Handling Retries and Network Intermittency

Network instability is an inevitable reality. When uploading large files, your application must be prepared to handle transient failures. Implementing an exponential backoff strategy for retries is essential. If a chunk upload fails, do not immediately retry; wait for a period that increases with each failure. This prevents overwhelming the S3 API and gives the user’s network a chance to stabilize. Most modern browser fetch implementations can be wrapped in a retry logic that checks for specific error codes before deciding to re-attempt the upload.

Furthermore, provide the user with the ability to pause and resume the upload. By storing the list of successfully uploaded chunks in the browser’s local storage, you can effectively resume a multi-gigabyte upload after a page refresh or a temporary disconnect. This level of UX polish is expected in professional software and significantly improves user retention during long-running background tasks.

Testing and Verification Strategies

Testing the integration between Next.js and S3 requires more than unit tests. You need integration tests that actually interact with a test bucket. Use a dedicated staging bucket for these operations. Your test suite should simulate various file sizes, including edge cases like zero-byte files and extremely large assets. Additionally, ensure that your tests verify the security of the generated URLs by attempting to access them from an unauthorized IP or after the expiration time has elapsed.

Consider using local emulation tools like LocalStack to mock S3 in your CI/CD pipeline. This allows you to run full end-to-end tests for your upload logic without incurring real storage costs or needing actual AWS credentials during the build phase. This approach ensures that your code is logically sound and compliant with the S3 API before it ever touches production infrastructure.

Architectural Convergence and Next Steps

By offloading file ingestion to S3, you create a scalable architecture that can grow alongside your user base without putting undue pressure on your Next.js application server. This separation of concerns—where your server manages authorization and S3 manages storage—is a hallmark of modern cloud-native design. As you scale, you may find that further optimizations, such as using CloudFront for accelerated uploads via Transfer Acceleration, become necessary. Always prioritize the stability of the data path over the simplicity of the implementation.

For further reading and to deepen your understanding of advanced Next.js patterns, we recommend reviewing our broader architectural documentation. [Explore our complete Next.js — Advanced directory for more guides.](/topics/topics-next-js-advanced/)

Factors That Affect Development Cost

  • AWS S3 storage consumption
  • Data transfer egress fees
  • API request volume for signature generation

Costs scale linearly with data volume and frequency of operations, making direct-to-S3 uploads a cost-efficient strategy compared to compute-heavy proxy methods.

Frequently Asked Questions

Why should I use presigned URLs instead of uploading directly to my Next.js server?

Uploading directly to your server consumes memory, blocks compute resources, and hits strict request size limits. Presigned URLs allow the client to upload directly to S3, bypassing your server and improving scalability.

How long should my presigned URL be valid for?

A validity period of 30 to 60 minutes is standard for most web applications. This provides enough time for the upload to complete while minimizing the security window for potential misuse.

What should I do if my large file upload fails?

Implement an exponential backoff retry strategy on the client side. For very large files, use the multipart upload API to ensure you can resume failed chunks rather than restarting the entire transfer.

Implementing direct S3 uploads via presigned URLs is a critical optimization for any Next.js application handling large assets. By moving the data transfer workload to the client and leveraging AWS infrastructure, you eliminate server-side bottlenecks, reduce compute costs, and significantly improve the reliability of your file ingestion pipeline. This pattern requires meticulous attention to IAM policies, CORS configuration, and error handling, but the resulting scalability is unmatched by traditional proxy-based approaches.

As you continue to refine your architecture, remember to monitor the health of your uploads through client-side telemetry and ensure your security policies remain tight. This systematic approach will ensure that your application remains performant and secure, regardless of the scale of your data operations.

NR Tech Studio builds custom web apps, mobile apps, SaaS platforms, and internal tools for growing businesses. If you’re working through a technical decision, feel free to reach out — no commitment required.

References & Further Reading