S3-compatible storage is object storage that implements the Amazon S3 HTTP API (the same operations, request signing and error format), so code written with an S3 SDK can store and read files on it by changing only the endpoint, region and keys.
Amazon S3 set the interface. Cloudflare R2, MinIO, DigitalOcean Spaces, Backblaze B2, Wasabi and others implement it. This entry is part of the screenshot glossary.
What it means in plain terms
Object storage keeps files ("objects") in flat containers ("buckets") under string names ("keys"), such as screenshots/2026/10/15/a1b2.png. You do not mount it like a disk. You send HTTP requests: PUT to store, GET to read, HEAD to check, DELETE to remove.
Because so many tools already speak S3, other vendors copied the API instead of inventing their own. "S3-compatible" means your S3 SDK, CLI and backup tools work against them. It does not mean every S3 feature exists everywhere. The core object operations are safe. Extras such as ACLs, lifecycle rules, event notifications and checksum headers vary by provider.
How a screenshot reaches your bucket
flowchart LR A[Your app] -- POST /v1/screenshot with storage --> B[RenderScreenshot API] B -- render --> C[Browser worker] C -- image --> D[(R2: our CDN copy)] B -- image + X-Cache-URL --> A B -. background job, PutObject .-> E[(Your S3-compatible bucket)]
That is how RenderScreenshot's storage feature works today. The renderer saves every image to our own R2 bucket, which backs the CDN link in X-Cache-URL. When you enable custom storage, a background job copies the image to your bucket after the API has answered. A slow or failing bucket never delays or fails the screenshot response.
How it works technically
Three settings decide whether an S3 client can talk to a given provider:
- Endpoint. AWS derives the host from the region. Everyone else needs an explicit URL, for example
https://<account_id>.r2.cloudflarestorage.comfor R2 orhttps://<region>.digitaloceanspaces.comfor Spaces. - Addressing style. AWS prefers virtual-host style (
bucket.host/key). Many compatible stores expect path style (host/bucket/key). When the endpoint is custom, our storage client turns path style on. - Region. SigV4 signs the region into every request. AWS needs the real region. Many compatible stores accept
auto, which is what our client sends when you give an endpoint and no region.
Our connection test in the dashboard is a HeadBucket request. S3 answers it with a bare 403 and no message when the keys lack permission, so "Forbidden" there almost always means wrong keys or a missing ListBucket permission.
How to set it
Puppeteer: capture and upload
import puppeteer from 'puppeteer'; import { S3Client, PutObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3'; const endpoint = process.env.S3_ENDPOINT; // unset for AWS; set for R2, MinIO, Spaces, B2... const s3 = new S3Client({ region: process.env.S3_REGION ?? (endpoint ? 'auto' : 'us-east-1'), endpoint, forcePathStyle: Boolean(endpoint), // bucket in the path, not the hostname }); const Bucket = process.env.S3_BUCKET; const browser = await puppeteer.launch(); let png; try { const page = await browser.newPage(); await page.setViewport({ width: 1200, height: 630 }); await page.goto('https://example.com', { waitUntil: 'load' }); png = await page.screenshot({ type: 'png' }); } finally { await browser.close(); } const Key = `screenshots/${new Date().toISOString().slice(0, 10)}/example.png`; await s3.send(new PutObjectCommand({ Bucket, Key, Body: png, ContentType: 'image/png', CacheControl: 'public, max-age=86400', })); const head = await s3.send(new HeadObjectCommand({ Bucket, Key })); console.log(`uploaded s3://${Bucket}/${Key}: ${head.ContentLength} bytes, ${head.ContentType}`);
The SDK reads AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from the environment, whichever provider issued them.
Playwright
The storage half is identical. Only the capture changes, because Playwright sets the viewport on the page or context:
const page = await browser.newPage({ viewport: { width: 1200, height: 630 } }); await page.goto('https://example.com'); const png = await page.screenshot(); // a Buffer, ready for PutObjectCommand
Tested on 2026-10-15 with Node 22.18.0, [email protected], [email protected] and @aws-sdk/client-s3 3.1152.0, against a local moto 5.2.4 S3 server with S3_ENDPOINT=http://127.0.0.1:5055. The upload printed 70253 bytes, image/png. To hand the upload to an untrusted worker without keys, use a presigned URL instead.
RenderScreenshot
Save the bucket name, endpoint, region and keys once in the storage dashboard, then add storage to a request to choose the object path and ACL:
curl -sS --fail -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com", "preset": "og_card", "storage": {"enabled": true, "path": "screenshots/{year}/{month}/{day}/{hash}.{ext}", "acl": "private"}}' \ -o card.png
The path must contain {hash} and {ext}. With the Node.js SDK the same options are .storageEnabled(), .storagePath('...') and .storageAcl('private'). Custom storage is on paid plans; the storage reference lists every provider's endpoint format, and the upload screenshots to S3 guide walks through bucket policies.
Common mistakes
- Forgetting the endpoint. The SDK silently talks to AWS and fails with an unknown-key error.
- Virtual-host style on a store that does not support it. DNS errors or 404s for the bucket. Turn on path style.
- Assuming every S3 feature exists. Test ACLs, lifecycle rules and checksum headers on your provider before you rely on them.
- Public buckets for private captures. Screenshots of logged-in pages can hold personal data. Keep them
privateand serve them with short-lived links. - Uploading cache hits again. Only fresh renders need a new copy; our storage job skips cache hits for that reason.
Related terms
- Presigned URL: give one upload or download to a client without keys.
- CDN edge cache: how the
X-Cache-URLcopy is served. - Cache TTL: why the CDN copy expires and your bucket copy does not.
- Web archiving: the use case that most needs long-term storage.
For the cost side of R2 against S3, read R2 vs S3 for screenshot storage. To try the storage options, create an account and connect a bucket.