Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The safe pattern is a two-step upload: your backend creates a short-lived presigned PUT URL for a unique object key, and the browser uploads the screenshot bytes to that URL. Storage credentials never reach the browser. Sign the expected Content-Type, configure bucket CORS for your application origins, and validate type, size, and naming on the backend. For ordinary screenshots, one PUT is simplest; use multipart upload when you need resumability or parallel transfers.

How the presigned-upload flow works

  1. Capture or receive the screenshot. The browser has PNG, JPEG, or WebP bytes (for example, from a canvas or a file picker).
  2. Ask your trusted backend for an upload URL. The backend chooses a non-guessable object key, checks the requested type and size, and signs a URL for one object and one operation.
  3. Upload directly from the browser. The browser sends an HTTP PUT to the returned URL with the exact signed Content-Type.
  4. Tell your backend the result. Store the key and metadata only after the upload succeeds, or have the backend verify the object with a HEAD request.

Amazon S3 presigned URLs bind a bucket, key, HTTP method, and expiration. Cloudflare R2 describes them as temporary authorization for a specific object operation; R2 supports GET, PUT, HEAD, and DELETE, but not HTML-form POST multipart uploads.

Choose the upload size and protocol

Situation Recommended method Why
Normal browser screenshots Single presigned PUT One request, simple error handling, and no upload-session bookkeeping.
Files approaching or below about 100 MB on R2 Single PUT Cloudflare’s 2026 guidance favors single uploads for small and medium objects.
Large files, unstable connections, or a need to resume Multipart upload Parts can transfer independently and failed parts can be retried without restarting the whole object.

Cloudflare R2 documents a 5 GiB maximum for a single upload and a 5 TiB maximum multipart object. Multipart uploads may contain up to 10,000 parts, each 5 MiB to 5 GiB. Check the limits and multipart API details of your particular S3-compatible provider before relying on these figures.

Backend: issue a narrowly scoped presigned URL

Only a trusted server should hold S3-compatible credentials. The server should accept a small allow-list of image types, impose a byte limit, generate the key itself, and return only the URL and key needed by the client. Never let a client submit an arbitrary bucket, key, expiration, or signed headers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall
Sale
Seagate 2TB Portable Hard Drive | USB 3.0 (STGX2000400)
  • Easily store and access 2TB to content on the go with the Seagate Portable Drive, a USB external hard drive
  • Designed to work with Windows or Mac computers, this external hard drive makes backup a snap just drag and drop
  • To get set up, connect the portable hard drive to a computer for automatic recognition no software required
  • This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable
  • The available storage capacity may vary.

Example Node.js signer

This example uses the AWS SDK v3 packages, which also work with many S3-compatible endpoints when you supply that provider’s endpoint and region. Install the packages with npm install express @aws-sdk/client-s3 @aws-sdk/s3-request-presigner.

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

const app = express();
app.use(express.json());

const bucket = process.env.S3_BUCKET;
const client = new S3Client({
  region: process.env.S3_REGION || 'auto',
  endpoint: process.env.S3_ENDPOINT || undefined,
  forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY_ID,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY
  }
});

const allowed = new Set(['image/png', 'image/jpeg', 'image/webp']);
const maxBytes = 20 * 1024 * 1024;

app.post('/api/screenshot-upload', async (req, res) => {
  const { contentType, size } = req.body || {};
  if (!allowed.has(contentType)) {
    return res.status(400).json({ error: 'Unsupported image type' });
  }
  if (!Number.isInteger(size) || size < 1 || size > maxBytes) {
    return res.status(400).json({ error: 'Invalid image size' });
  }

  // Include an authenticated user or tenant identifier in this prefix.
  const key = `screenshots/${crypto.randomUUID()}`;
  const command = new PutObjectCommand({
    Bucket: bucket,
    Key: key,
    ContentType: contentType
  });
  const url = await getSignedUrl(client, command, { expiresIn: 300 });
  res.json({ url, key, contentType, expiresIn: 300 });
});

app.listen(3000, () => console.log('Listening on http://localhost:3000'));

Set S3_ENDPOINT for a non-Amazon provider, and follow that provider’s documented region and path-style requirements. The five-minute lifetime is an example; use the shortest period that covers your user flow. The URL is a bearer token: anyone who obtains it can perform the signed operation until it expires.

Validation in this endpoint is not a substitute for checking the resulting object. A malicious client can lie about a declared MIME type or size, so inspect uploaded objects asynchronously (or with a trusted image parser) before making them public.

Rank #2
Seagate Portable 5TB External Hard Drive HDD – USB 3.0 for PC, Mac, PS4, & Xbox - 1-Year Rescue Service (STGX5000400), Black
  • Easily store and access 5TB of content on the go with the Seagate portable drive, a USB external hard Drive
  • Designed to work with Windows or Mac computers, this external hard drive makes backup a snap just drag and drop
  • To get set up, connect the portable hard drive to a computer for automatic recognition software required
  • This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable
  • The available storage capacity may vary.

Browser: request the URL and upload the bytes

Uploading a selected screenshot with Fetch

async function uploadScreenshot(file) {
  const permitted = new Set(['image/png', 'image/jpeg', 'image/webp']);
  if (!permitted.has(file.type)) throw new Error('Unsupported image type');
  if (file.size > 20 * 1024 * 1024) throw new Error('Screenshot is too large');

  const ticketResponse = await fetch('/api/screenshot-upload', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ contentType: file.type, size: file.size })
  });
  if (!ticketResponse.ok) throw new Error('Could not obtain upload URL');
  const { url, key } = await ticketResponse.json();

  const uploadResponse = await fetch(url, {
    method: 'PUT',
    headers: { 'Content-Type': file.type },
    body: file
  });
  if (!uploadResponse.ok) {
    throw new Error(`Storage returned HTTP ${uploadResponse.status}`);
  }
  return key;
}

const input = document.querySelector('#screenshot');
input.addEventListener('change', async () => {
  const key = await uploadScreenshot(input.files[0]);
  console.log('Uploaded object key:', key);
});

The header must match the value used while signing. Do not add an Authorization header to the presigned request unless your provider explicitly requires one; the signature is the authorization.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Command-line tests

Upload with cURL

After your backend returns a URL, test the storage side independently of browser CORS:

curl -X PUT 
  -H 'Content-Type: image/png' 
  --upload-file screenshot.png 
  'PASTE_THE_PRESIGNED_PUT_URL_HERE'

A successful upload commonly returns an empty response with a success status. Use the provider’s object listing or a signed HEAD/GET request to verify the key.

Rank #3
Seagate Portable 1TB External Hard Drive HDD – USB 3.0 for PC, Mac, PlayStation, & Xbox, 1-Year Rescue Service (STGX1000400) , Black
  • Easily store and access 1TB to content on the go with the Seagate Portable Drive, a USB external hard drive.Specific uses: Personal
  • Designed to work with Windows or Mac computers, this external hard drive makes backup a snap just drag and drop. Reformatting may be required for Mac
  • To get set up, connect the portable hard drive to a computer for automatic recognition no software required
  • This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable
  • The available storage capacity may vary.

Upload from Python

import requests

with open('screenshot.webp', 'rb') as image:
    response = requests.put(
        'PASTE_THE_PRESIGNED_PUT_URL_HERE',
        data=image,
        headers={'Content-Type': 'image/webp'},
        timeout=90,
    )
response.raise_for_status()
print('uploaded')

Configure bucket CORS before using a browser

CORS is a browser policy, not storage authentication. Permit only the origins that actually host your application, and list the methods and request headers your upload uses. A practical rule is:

  • Allowed origins: your production origin and a separately listed development origin, not * when credentials or private data are involved.
  • Allowed methods: PUT; add HEAD or GET only if the browser performs those operations.
  • Allowed request headers: at least Content-Type; include any provider-required checksum header.
  • Exposed response headers: expose an ETag only if your client needs to read it.
  • Max age: cache preflight responses for a reasonable period, then reduce it while debugging.

CORS configuration differs between S3-compatible services, so use the exact JSON or UI format documented by your provider. A command-line PUT succeeding while Fetch fails almost always indicates an origin, method, or header mismatch rather than a bad presigned URL.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Multipart uploads for resumability

Multipart is a session, not one presigned URL. Your backend starts an upload and returns an upload ID. It then signs a URL for each numbered part (or otherwise authorizes each part), while the browser uploads contiguous parts independently. Record each successful part’s number and ETag. Finally, the backend completes the upload in part-number order. If a part fails, retry only that part.

Rank #4
Seagate Portable 4TB External Hard Drive HDD – USB 3.0, 1-Year Rescue
  • Easily store and access 4TB of content on the go with the Seagate Portable Drive, a USB external hard drive.Specific uses: Personal
  • Designed to work with Windows or Mac computers, this external hard drive makes backup a snap just drag and drop
  • To get set up, connect the portable hard drive to a computer for automatic recognition no software required
  • This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable
  • The available storage capacity may vary.
  1. Validate the final size and type before creating the session.
  2. Create a multipart upload and persist its upload ID with the user and object key.
  3. Choose a part size within your provider’s limits; larger parts reduce request count, while smaller parts improve retry granularity.
  4. Upload parts with bounded concurrency rather than opening hundreds of simultaneous requests.
  5. Persist part numbers and ETags as they finish.
  6. Complete the upload only with the recorded parts, or abort the session when the user cancels.

Schedule cleanup for abandoned sessions according to your provider’s lifecycle policy. An incomplete multipart upload can consume storage even though no final object is visible.

Security and integrity checklist

  • Keep access keys exclusively on the backend and give them the minimum bucket permissions needed to create the intended objects.
  • Use a random key under a user- or tenant-specific prefix; never use an email address or sequential ID as the sole key.
  • Keep expiration short and sign only PUT for uploads. Do not issue a URL that also permits deletion unless that is required.
  • Bind and verify Content-Type, enforce a maximum size, and reject unexpected extensions or metadata.
  • Treat every presigned URL as a bearer token. Do not log it in analytics, expose it in a public page, or send it to an untrusted service.
  • Use object ownership and access controls that keep new screenshots private until your application authorizes viewing.
  • For sensitive screenshots, scan or decode the image server-side and strip metadata before publication.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If you still need to create the screenshot, ScreenshotNeo can return a PNG, JPEG, WebP, or PDF from one API request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

Save the returned bytes to your own S3-compatible bucket using the presigned flow above. ScreenshotNeo supports full-page captures with lazy images loaded, CSS-selector element captures, dark mode, device presets and custom viewports, retina scale, PDF paper and page options, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for parameters and response headers. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000, and every feature is available on every plan. Create a free ScreenshotNeo account and then upload the resulting file to your bucket with the presigned URL.

Best Value
Sale
UnionSine 500GB Ultra Slim Portable External Hard Drive HDD-USB 3.0
  • [Upgraded Version] - This external hard drive features a mirrored logo stripe combined with a striped anti-slip design, and the rounded corners of the casing make it easier to grip. The stripes also have a heat dissipation function, ensuring stable and fast data transfer.
  • 【Ultra-thin and quiet】 - The motherboard adopts JMicron 578 noise-free solution, giving you a quiet working environment. Lightweight and portable size designed to fit in your pocket for easy portability.
  • 【Ultra-Fast Data Transfers】 - Pairing this external hard drive with JMicron 578 solution USB 3.0 and USB 2.0 interfaces enables blazing-fast data transfer. It boasts theoretical read speeds of up to 125MB/s and write speeds of up to 103MB/s.
  • 【Plug and Play】 - With no software to install, just plug it in and the drive is ready to use.The hard disk chip is wrapped with an aluminum anti-interference layer to increase heat dissipation and protect data.
  • 【What You Get】 - 1 x Portable Hard Drive, 1 x USB 3.0 Cable, 1 x User Manual, Gift-type shell packaging ,Three-year manufacturer's warranty and free technical support services.

Troubleshooting failed uploads

“SignatureDoesNotMatch” or HTTP 403

  • Ensure the HTTP method is exactly PUT, the URL is unmodified, and the signed Content-Type exactly matches the request (including spelling and case).
  • Check the server clock, URL expiration, endpoint, region, and path-style setting.
  • Do not append query parameters or add headers that your signer included or excluded unexpectedly.

Browser reports a CORS error

  • Confirm the page’s origin is listed exactly, including scheme and port.
  • Allow PUT and Content-Type, then test again after the provider’s CORS configuration propagates.
  • Compare with cURL: if cURL fails too, fix signing or permissions first.

Upload succeeds but the image displays incorrectly

  • Make sure the bytes are the original image, not a base64 data URL string.
  • Sign and send the real MIME type, and avoid declaring PNG for JPEG bytes.
  • Check that your application is reading the correct key and that the object was not overwritten.

Multipart upload never completes

  • Persist every part ETag and use the exact part numbers during completion.
  • Retry failed parts, then abort abandoned upload IDs so they do not accumulate.
  • Keep concurrency bounded and use a part size accepted by the provider.

Cost, latency, and reliability decisions

The upload request itself is only one part of operating cost. Compare providers on S3 API compatibility, endpoint and region requirements, presigned URL behavior, CORS, single- and multipart-size limits, resumability, lifecycle cleanup, integrity checks, latency, storage pricing, egress pricing, and any partner-program terms. A direct browser upload reduces backend bandwidth and usually shortens the path, but your application still pays storage and any subsequent download or egress charges.

For reliability, create keys idempotently, record the upload state, retry transient failures with backoff, and verify completion before notifying users. A short URL lifetime limits abuse, while a lifetime that is too short causes failures on slow connections; choose it from observed upload duration rather than using a permanent URL.

FAQ

Can I upload without exposing AWS or storage credentials?

Yes. A trusted backend signs a URL for one object and operation, and the browser uses that URL without receiving credentials.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should the browser use POST instead of PUT?

Use the method your provider and signature support. Cloudflare R2 presigned URLs support PUT but not HTML-form POST multipart uploads, so a browser upload to R2 should use PUT.

When should I make an uploaded screenshot public?

Keep the object private by default. Return it through an authenticated application endpoint or a separate short-lived download URL after authorization.

Quick Recap

SaleBestseller No. 1
Seagate 2TB Portable Hard Drive | USB 3.0 (STGX2000400)
Seagate 2TB Portable Hard Drive | USB 3.0 (STGX2000400)
This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable; The available storage capacity may vary.
$129.99
Bestseller No. 2
Seagate Portable 5TB External Hard Drive HDD – USB 3.0 for PC, Mac, PS4, & Xbox - 1-Year Rescue Service (STGX5000400), Black
Seagate Portable 5TB External Hard Drive HDD – USB 3.0 for PC, Mac, PS4, & Xbox - 1-Year Rescue Service (STGX5000400), Black
This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable; The available storage capacity may vary.
$229.99
Bestseller No. 3
Seagate Portable 1TB External Hard Drive HDD – USB 3.0 for PC, Mac, PlayStation, & Xbox, 1-Year Rescue Service (STGX1000400) , Black
Seagate Portable 1TB External Hard Drive HDD – USB 3.0 for PC, Mac, PlayStation, & Xbox, 1-Year Rescue Service (STGX1000400) , Black
This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable; The available storage capacity may vary.
$119.80
Bestseller No. 4
Seagate Portable 4TB External Hard Drive HDD – USB 3.0, 1-Year Rescue
Seagate Portable 4TB External Hard Drive HDD – USB 3.0, 1-Year Rescue
This USB drive provides plug and play simplicity with the included 18 inch USB 3.0 cable; The available storage capacity may vary.
$189.99

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.