R2 is object storage. A Worker reaches
a bucket through an r2_buckets binding. celld stores the objects in the fleet
bucket that the node opened with --bucket, so R2 has the durability of that
object store. On a node without a fleet bucket, every R2 method fails. Read the
Workers API reference
for the API.
The R2 example reads, writes, and deletes objects in an R2 bucket.
get(key, { range, onlyIf })returns anR2ObjectBodywith a streamed body, ornull. A range is anoffset, alength, asuffix, or aRangeheader.head(key)returns the object record without the body.put(key, value, { httpMetadata, customMetadata, onlyIf })takes a string, anArrayBuffer, a typed array, aBlob, or aReadableStream. A refused condition returnsnull.delete(keys)removes one key or a list of keys.list({ prefix, delimiter, cursor, startAfter })returns a page of objects.createMultipartUpload(key)returns a handle.uploadPart(number, value)sends one part, andcomplete(parts)assembles the object.
Each binding has its own bucket_name, and celld stores key <key> as the
object r2/<bucket_name>/<key>. A key is 1 to 1024 bytes. A streamed put()
body that grows past 8 MiB becomes a multipart upload. delete() removes up to
1000 keys in one call. celld has no public bucket URL, no presigned URL, and no
S3 endpoint, so put a Worker in front of the bytes to publish them.
The object store keeps some keys in a percent-encoded form: a key with a
non-ASCII character, a control character, or one of
% \ { } ^ ` [ ] " < > ~ # | * ?. For example, přehled.html becomes
r2/<bucket_name>/p%C5%99ehled.html. Each empty segment becomes %, so
photos/ becomes r2/<bucket_name>/photos/%, and a/b, /a/b, a//b, and
a/b/ are four different objects, as in Cloudflare R2. list() returns the
decoded key, but it sorts by the encoded form and compares startAfter with the
encoded form. Cloudflare R2 sorts by UTF-8 bytes. For example, Cloudflare R2
returns a~ after startAfter: "aa", but celld compares a%7E with aa and
skips the key. A cursor from a celld listing does not have this problem.
list() fails when it reaches an object that another tool wrote with a
percent-encoded key that is not UTF-8, such as a%FF.
celld 0.5.1 and earlier removed each empty segment from a key. An object that
such a version wrote under photos/ stays under photos, and list() returns
it as photos.
A multipart upload lives on the node that opened it. Read Multipart upload for the part rules. A part that arrives before its predecessor waits in memory, up to 256 MiB, and celld then refuses a further part. A waiting part can be replaced. A part that already went to the object store cannot, and celld refuses that write.
- An R2 binding uses the fleet bucket under
r2/<bucket_name>/. - The five content headers are stored as headers.
customMetadata,cacheExpiry, the checksums, and the storage class are stored together as one JSON value, under thecelld-r2user-metadata name (celld_r2on Azure Blob Storage, which refuses a hyphen in a metadata name). - An object another tool wrote still reads through the binding. Its user
metadata becomes its
customMetadata, and its headers become itshttpMetadata. Usecelld r2 putto write the complete record. celld r2 get|head|put|delete|listreplaceswrangler r2 object. The command reads the fleet bucket directly, so it needs no running node.- An object
versionequals its content ETag, so identical content produces the same version. ssecKeyandjurisdictionare unavailable.- A conditional write cannot use a streamed body larger than 8 MiB.
createMultipartUpload()does not accept a checksum.- A multipart upload cannot resume on another node or after a restart. celld also cannot replace a part that the object store already holds.
- Out-of-order parts can use at most 256 MiB of memory, and completion cannot change the stored part order.
The Cloudflare compatibility page lists the runtime APIs and the unsupported services.