Object Storage & Blob Storage • Presigned URLs & Access ControlHard⏱️ ~3 min
Common Failure Modes and Operational Edge Cases
Presigned URLs fail in production for reasons that rarely appear in tutorials: clock skew, header normalization, Cross Origin Resource Sharing (CORS) preflight mismatches, and multipart signature storms. Understanding these edge cases is critical for building resilient systems at scale.
Clock skew causes mysterious 403 Signature Expired or SignatureDoesNotMatch errors. Presigned URLs encode an absolute expiration timestamp; verification compares request time against that timestamp. If a client device clock is 10 minutes fast, or your signing service clock drifts 30 seconds slow, requests fail even though the Time To Live (TTL) appears valid. Mobile devices on poor networks may take 2 to 5 minutes to complete an upload; if your TTL is 2 minutes and the clock drifts 1 minute, uploads fail mid flight. Mitigation: use sufficient TTL buffers (at least 3 to 5 minutes for uploads accounting for retry), support resumable or multipart uploads where each chunk can get a fresh URL, and monitor 403 error rates by reason code to detect systemic clock issues.
Cross Origin Resource Sharing (CORS) and preflight mismatches are the second most common production issue. Browsers send OPTIONS preflight requests before PUT or POST with custom headers. If your bucket CORS configuration does not allow the exact HTTP method, headers, and origin, the preflight fails and the upload never starts. Worse, if you sign headers like Content-MD5 or custom metadata but your CORS rules do not expose those headers, the browser strips them and the signature fails. Best practice: keep signed headers minimal (ideally none beyond Content-Type and Content-Length if required), explicitly allow those headers in CORS, and test with actual browser clients not just curl.
Multipart signature storms occur when many users simultaneously upload large files split into hundreds of parts. Consider 1,000 concurrent 5 GB video uploads at 256 parts each: that is 256,000 signatures over a few minutes, potentially thousands of queries per second (QPS) to your signing service. If each signature takes 5 milliseconds server side and you have 4 virtual Central Processing Units (vCPUs), you are bottlenecked at approximately 800 signatures per second per instance. Mitigation strategies include using larger part sizes (64 MB instead of 16 MB to reduce part count by 4x), client side concurrency throttling, or switching to session token flows where a single credential can sign multiple parts client side without per part server round trips.
Incomplete bucket and object permissions cause subtle Access Denied errors even with valid signatures. Object storage systems typically require permissions at both bucket level (ListBucket, possibly CreateBucket for first write) and object level (PutObject, GetObject). Missing either results in 403 even though the signature itself is correct. Additionally, explicit Deny rules in bucket policies override presigned URL Allow rules in Amazon S3, a common surprise. Validate your permission matrix with least privilege: grant only what the signing principal needs on specific prefixes, and deny everything outside those prefixes.
💡 Key Takeaways
•Clock skew buffer: For uploads that may take 2 to 5 minutes on slow networks, use 5 minute Time To Live (TTL) minimum. Monitor 403 errors by reason; spike in Expired errors suggests clock drift. For long uploads, prefer resumable or multipart where each chunk fetches a fresh URL rather than relying on a single long lived presigned URL.
•CORS preflight alignment: Explicitly allow in bucket CORS rules the exact methods (PUT, POST), headers (Content-Type, Content-Length, any custom headers you sign), and origins you use. Test with browser DevTools network tab; preflight OPTIONS failures appear as CORS errors before the signed request even attempts.
•Multipart QPS capacity: A 5 GB upload at 16 MB parts equals 320 signatures. At 1,000 concurrent uploads, that is 320,000 signatures in minutes (approximately 1,000 to 5,000 QPS burst). Plan capacity for 10x to 20x average QPS, or architect to amortize signatures: batch generate all part URLs in single API call, use larger parts, or issue session credentials that clients use to self sign parts.
•Bucket and object permission matrix: Signing principal needs PutObject on objects, sometimes ListBucket on bucket. Explicit Deny in bucket policy overrides presigned Allow. Test with least privilege role: grant only specific prefixes, deny outside those. Validate that actual upload succeeds end to end, not just that signature generates.
•URL leakage in logs and telemetry: Application logs, error tracking services (Sentry, Rollbar), and analytics often capture full URLs including query strings with signatures. An attacker reading logs can replay URLs until expiration. Redact query parameters in structured logging, disable third party script access to presigned URLs, and keep TTLs short to minimize exposure window.
📌 Examples
Clock skew debugging: User reports upload fails immediately with 403 Expired. Check server logs: X-Amz-Date in request is 15 minutes in future relative to server time. User device clock is wrong. Solution: educate user or increase TTL buffer to 10 minutes for mobile clients on unreliable networks.
CORS preflight failure: Browser console shows CORS error before 403. OPTIONS request to bucket returns 403 or empty Access-Control-Allow-Headers. Bucket CORS allows PUT but not the Content-MD5 header you signed. Remove Content-MD5 from signature or add to CORS AllowedHeaders.
Multipart signature storm: 5,000 concurrent large uploads. Signing service latency spikes from 5 ms p50 to 500 ms p99; autoscaler cannot keep up. Switch from 16 MB parts (320 per 5 GB) to 64 MB parts (80 per 5 GB), reducing signature load by 4x. Alternatively, issue temporary AWS Security Token Service (STS) credentials valid for 1 hour; clients self sign part URLs locally, eliminating server signing QPS entirely.
Bucket policy Deny override: Generate valid presigned URL for uploads/user-123/file.jpg. Upload fails with 403 Access Denied despite correct signature. Bucket policy has explicit Deny for uploads/* prefix due to misconfiguration. Deny overrides presigned Allow; fix policy to Deny only prefixes outside user namespaces.