This ADR defines how a client deploys a scene whose content is too large for a single request,
by splitting it across several POST /entities requests. Each request carries the
multipart field partial=true, declared also in the URL as
?partial=true, and a subset of the files. The signed entity ID identifies the
upload, so there is no session creation or explicit commit step. The server answers
202 with the hashes it still needs until the last one arrives, then validates and
publishes the scene in that same request and answers 200. Uploads that target
overlapping parcels coexist instead of evicting each other; which one ends up live is decided
at publication by entity timestamp order. Staged content is private to the receiving server
until publication and is never synchronized. The protocol applies to Catalyst content servers
and to the Worlds content server.
A deployment is a single multipart POST /entities request carrying the entity
file, its auth chain and every content file that is not already stored (see
ADR-45 and ADR-51). The infrastructure in
front of the Decentraland content servers times out requests larger than roughly 300 MB, so
large scenes cannot be deployed at all, and a transient failure near the end of a large upload
forces the creator to send everything again.
Partial deployments fix both problems. Content is sent in bounded batches, a failed batch is retried on its own, and progress survives dropped connections, client restarts and server restarts until the upload expires.
This affects every deployment client (the Creator Hub, the SDK CLI and
dcl-catalyst-client) and both content server implementations. Clients need one
protocol that behaves the same way against a Catalyst and a Worlds server, so the rules are
specified here instead of per implementation.
Vocabulary:
POST /entities request.
Explicit sessions. A POST /uploads endpoint returning an upload ID,
followed by PUT requests per file and a final commit. This adds endpoints, a
second identifier and a separate commit failure mode. The signed entity ID already identifies
the upload and already proves the signer's intent, so a separate session ID adds nothing.
One pending upload per parcel set. The first implementations let a newer upload on overlapping parcels replace an older one and discard its staged content. Two creators, or one creator with two tabs, could keep evicting each other, and an upload's progress could vanish between two batches. That forced clients to re-check their own state on every batch. It was replaced by the coexistence model below, which bounds abuse with explicit quotas instead of eviction.
Chunked single files. Splitting individual files across requests would remove the per-file size limit, but it requires byte-range assembly and partial-hash state on the server. Files stay atomic; a single file must fit in one request.
Chosen: entity-keyed batches. The existing endpoint gains one field, mirrored by a query parameter. Uploads are keyed by the signed entity ID, coexist within quotas, and publish on the completing request. A server without support rejects the first multi-batch request instead of misbehaving silently.
400.
A batch is a regular multipart POST /entities?partial=true request with these
fields:
| Field | Required | Description |
|---|---|---|
entityId |
Yes | The entity ID. Identifies the upload. |
authChain |
Yes |
The auth chain signing entityId, exactly as in a regular deployment. Sent
on every batch.
|
partial |
Yes | The literal string true. |
<entityId> file |
First batch | The entity file. The signer who started the upload MAY omit it on later batches while the upload is live. |
<hash> files |
No | Content files, keyed by their content hash. Any subset of the entity's content. |
And this query parameter:
| Parameter | Required | Description |
|---|---|---|
partial |
Recommended |
The literal string true. Declares the request as a batch before its body is
read. Any other value is ignored.
|
Rules:
400, because the upload's
reservations are charged to its creator.
partial=true query parameter and the
partial=true form field on every batch. The form field is what makes a request
a batch; the query parameter lets the server know before reading the body.
partial=true query parameter whose partial form
field is missing or not true MUST be rejected with 400, before it
is counted or deployed. The error message is
The 'partial=true' query parameter requires the 'partial=true' form field.
429 below).
entityId before using it. If it cannot, it answers
400 asking for the entity file; a later batch that includes the entity file
replaces the stored copy.
Content-Length on every batch. Servers bound uploads in
progress per client before reading the body, and without a declared length they MAY reserve
the client's whole allowance for one request.
| Status | Body | Meaning | Client action |
|---|---|---|---|
200 |
{ creationTimestamp, ...serviceFields } |
The entity is published, by this request or an earlier one. | Done. |
202 |
{ missing: string[] } |
The files in this batch are stored; the entity is not published.
missing lists the content hashes the server still needs.
|
Upload the hashes in missing. |
400 |
Error body | Terminal: validation failure, expired upload, a newer entity already live on these parcels (including an upload the server released for that reason, see Overlapping uploads), missing permission at publication, or a batch or upload that alone exceeds a quota (see Quotas). | Stop. Do not retry the same entity. |
413 |
Error body | The batch exceeds a request size or count limit (a file, the total body, or the number or size of form fields). Nothing from the batch is staged. | Stop; send smaller batches. Do not retry the same batch. |
408 |
Error body | The server's processing deadline elapsed, or the batch body was not received in time. Staged files persist. | Retry the batch, smaller if its body was too slow. |
409 |
Error body | Worlds only: the parcel replacement authorization changed while publishing. | Retry the batch. |
429 |
Error body, Retry-After header |
A quota is full (upload count, staged bytes, byte rate; see Quotas), or too many uploads in progress from the same client (concurrent requests or in-flight bytes). On Catalyst also: the per-pointer deployment rate limit, another deployment in progress on the same pointers, or the per-source daily deployment limit. Partial batches rejected before their upload is created count against that daily limit, and once a source has spent it, only batches that continue the signer's own live upload are accepted. Staged files persist. | Wait at least Retry-After, then retry. |
503 |
Error body, Retry-After header |
Transient server condition: the server's upload capacity is exhausted (bytes in flight, temporary storage or memory), a lock wait timed out, or publication failed for a server-side reason. Staged files persist and the upload stays live. | Wait at least Retry-After, then retry. |
Other 5xx / network error |
— | Transient. Staged files persist. | Retry with exponential backoff. |
Rules:
202 acknowledges storage, not publication. Clients MUST NOT report success on
202.
missing is authoritative for this upload. Clients MUST upload the hashes it
lists, including hashes they skipped because a global availability check
(/available-content) reported them present. Clients MAY use
/available-content to plan the first batch only.
Retry-After with every 429 and SHOULD send it
with every 503. Clients MUST NOT retry before it elapses.
200 body MUST contain creationTimestamp. Each server MAY add its
own fields (Worlds adds a preview message).
400 for a failure that a retry could fix, such as a
database or storage error while publishing, because clients stop on 400. It
answers 503 and keeps the upload.
sequenceDiagram
participant C as Client
participant S as Content server
C->>S: GET /available-content?cid=... (optional planning)
S-->>C: which hashes are already stored
C->>S: POST /entities?partial=true, entity file + batch 1
S-->>C: 202 { missing: [h2, h3, h4] }
par concurrent batches
C->>S: POST /entities?partial=true, batch 2 (h2, h3)
S-->>C: 202 { missing: [h4] }
and
C->>S: POST /entities?partial=true, batch 3 (h4)
S-->>C: 200 { creationTimestamp }
end
202 with what is still
missing. Batches for the same upload MAY be sent concurrently. The server serializes batches
for one entity; batches for different entities proceed in parallel.
200. Deployment permission is checked again here against current ownership, so
a creator who lost the land or name during the upload is rejected with 400. If
publication fails for a server-side reason instead, the server answers 503 and
the upload stays live, so the client can retry the completing batch.
200 with its creationTimestamp: the entity ID is the hash of the
entity file, so the live entity is exactly the one being uploaded. The server never
publishes the entity again. The signer that completed the upload also gets that
200 after the entity has been replaced or undeployed: Catalyst servers answer
it for as long as the deployment is recorded; Worlds servers keep a completion receipt for a
configurable period (default 24 hours). If the same entity ID is published again after an
undeploy, the new publication replaces that answer.
400. The client MUST create a new entity with a fresh
timestamp and signature. A server MUST NOT delete the staged content of an upload it can
still publish: the decision that an upload has expired MUST be the same for content cleanup
and for publication, for example by judging both on a single clock.
timestamp is
newer, and ties are broken by the greater entity ID. A completing request whose entity is
older than an entity already live on any of its parcels MUST be rejected with
400. The order in which uploads complete never lets an older entity overwrite a
newer one.
400. A server MUST NOT release an upload for a failure that may not be
permanent, such as a permission check that could not reach its data source.
Servers MUST bound staging with quotas. Per-account and per-server quotas MUST keep an expired upload charged, both its upload slot and its bytes, until its content is physically deleted. Servers SHOULD reclaim expired uploads within minutes of their expiry; both servers run cleanup every 5 minutes. Both servers use these defaults, and operators MAY configure them:
| Quota | Default |
|---|---|
| Uploads per account, including expired uploads awaiting cleanup | 10 |
| Staged bytes per account | 1 GiB |
| Staged bytes per server | 50 GiB |
| Accepted batch bytes per account per minute, retries included | 512 MiB |
| Upload lifetime | 1 hour |
| Worlds: live uploads per world, across accounts | 4, of which one account may hold at most 3 |
| Worlds: staged bytes per world, across accounts | 2 × the world's scene size cap |
| Catalyst: live uploads holding any one parcel, across accounts | 4, of which one account may hold at most 3 |
Deploy permission for a world or a parcel can be granted to any number of addresses, so per-account quotas alone let one world or parcel set take most of the server budget. The per-world and per-parcel quotas bound what any one world or parcel set can stage, however many accounts may deploy to it. They count only live uploads, so an expired upload stops blocking its world or parcels as soon as it expires. The Worlds byte quota uses the world's fixed scene size cap (its whitelist size, the ENS cap or the default scene cap), not the owner's remaining allowance, so every upload to a world sees the same budget.
Servers MAY also bound the uploads in progress from one client before their bodies are read,
and answer 429 when a client exceeds that bound. Only content an upload actually
stores counts against its staging quotas: files already present on the server count toward the
scene's size limit but not toward staged bytes, and a batch file the server already has is
dropped on receipt (its bytes still count toward the byte rate). A batch or upload that alone
exceeds a quota (a batch larger than the byte rate, or an upload whose own staged bytes exceed
the account, world or server budget) can never be admitted and is rejected with
400. A batch that would exceed a quota because of other uploads or traffic is
rejected with 429 before any of its files are stored, and its
Retry-After is the earliest time a retry can succeed: for the byte rate, which
uses a fixed one-minute window per account, the end of that window; for the upload count and
staged bytes, no earlier than when the oldest upload holding that quota expires, since expired
uploads stay charged until cleanup (servers MAY add their cleanup schedule). The error message
names the quota, its usage and its limit. Per-scene size limits are the same as for regular
deployments and are checked from the first batch, before any content beyond the limit is
stored.
/available-content, since
they are stored.
A conforming client:
/available-content to skip
files already stored.
200 as the result.
202 and none answered 200, sends the
hashes from the latest missing list and repeats until a 200 or a
terminal error.
408, 409, 429, 503, other
5xx and network failures with exponential backoff, never waiting less than
Retry-After. When Retry-After is longer than the client is willing
to wait, it stops and surfaces the server's error and the retry time instead of retrying
early.
400 and 413 and surfaces the server's error.Content-Length on every batch.Clients MAY use a regular single-request deployment when the scene is small enough that batching adds nothing; the SDK uses partial deployments from 50 MiB. Clients MAY report upload progress from the bytes they have sent; this is not part of the protocol.
A server that does not implement this ADR ignores partial, in the query and in
the form, and validates the first batch as a regular deployment. If that batch contains every
missing file it succeeds as a regular deployment; otherwise it answers 400 for
the missing content. Clients MAY treat that 400 as "partial deployments not
supported" and fall back to a regular deployment when the scene fits in one request.
400 described in Compatibility. A
positive signal, such as a field in the server's /about response, would let
clients choose the protocol before uploading.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.