Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

storage-s3: only the first upload in a process is stored (Local API, Cloudflare R2)

Open
#18,352 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
48/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
aws, node.js, typescript
Domain
backend, cloud

Research direction

Run the supplied upload-many.ts reproduction with s3Storage configured for Cloudflare R2, then inspect handleUpload and uploadFile in @payloadcms/storage-s3 while comparing the adapter calls with the direct S3 client. Done means sequential payload.create uploads store every object, including generated image sizes, and later documents do not resolve with missing files.

Written by the indexing model from the issue text.

Description

Bug
Describe the Bug

When several media documents are created in one Node process through the Local API, with @payloadcms/storage-s3 pointed at Cloudflare R2, only the first upload results in an object in the bucket.

Every later payload.create({ collection: 'media', file }) resolves normally: the document is created, doc.url is populated, no error is thrown and the process exits 0. The object simply is not in the bucket.

What I ruled out
Varied Result
File size (2 KB avatar → 40 KB PNG) one per process
Order (the "first" file differs each run as earlier ones succeed) always the first, whichever it is
clientUploads: true / false no difference
Checking immediately vs. retrying with cache-busting for 10s no difference — the object never appears
Local disk storage (no s3Storage) all 16 files stored in a single process
Same credentials/bucket/endpoint via @aws-sdk/client-s3 directly 3/3 stored, each verified with HeadObject

The direct-SDK check uses the same client options the adapter sets, so credentials, bucket, endpoint, region and the keep-alive agents are all exercised the same way:

new S3({
  credentials: { accessKeyId, secretAccessKey },
  endpoint: `https://${account}.r2.cloudflarestorage.com`,
  forcePathStyle: true,
  region: 'auto',
  requestHandler: {
    httpAgent: { keepAlive: true, maxSockets: 100 },
    httpsAgent: { keepAlive: true, maxSockets: 100 },
  },
})

Three sequential PutObjectCommand calls on one client: 3/3 stored. The same three files through payload.create: 1/3.

Note the first upload stores its generated image sizes too (name.png and name-300x300.png both land), so it isn't "one object per process" — it's one upload per process.

Reproduction
// npx payload run upload-many.ts, with s3Storage configured for R2
import config from '@payload-config'
import fs from 'fs/promises'
import { getPayload } from 'payload'

const payload = await getPayload({ config })

for (const name of ['a.png', 'b.png', 'c.png']) {
  const data = await fs.readFile(`./images/${name}`)
  const doc = await payload.create({
    collection: 'media',
    data: { alt: name },
    file: { data, mimetype: 'image/png', name, size: data.length },
  })
  const head = await fetch(doc.url, { method: 'HEAD' })
  console.log(name, doc.url, head.status) // a.png 200, b.png 404, c.png 404
}

media is a standard upload collection with imageSizes and, in my case, disablePayloadAccessControl: true plus a generateFileURL pointing at the bucket's custom domain.

Environment
  • payload 3.89.0
  • @payloadcms/storage-s3 3.89.0
  • @payloadcms/plugin-cloud-storage 3.89.0
  • @aws-sdk/client-s3 3.1130.0
  • @payloadcms/db-postgres 3.89.0 (Neon, Postgres 18)
  • Node 24.16.0, Ubuntu (GitHub Actions runner)
  • NODE_ENV=production
  • Storage: Cloudflare R2 via the S3 API
Why it matters

uploadFile awaits client.putObject(...), so a rejected PUT would surface. Since nothing rejects, the failure is silent: you end up with documents whose files do not exist, and nothing in the logs suggests it. In my case fifteen of sixteen images were lost this way and the site served broken images for days before anyone noticed.

I have not isolated whether handleUpload is invoked at all for the later files. Happy to add instrumentation or test a patch if that would help.

Workaround

One upload per process — run the import script repeatedly until nothing is left to store, and verify each object exists rather than trusting the resolved promise.

Dominant language
TypeScript
Stars
45.5k
Forks
4.3k
Avg merge
1d 19h
Merged PRs (30d)
299

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from payloadcms/payload

All issues in payloadcms/payload

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.