Larger, Cheaper Storage Batches
The September 3, 2026 storage upgrade makes it cheaper to manage many files together. Three changes work together: smaller on-chain piece records, smaller addition events, and less metadata overhead. More pieces can fit into one operation, and batching spreads the transaction’s fixed costs across more files.
For the full benefit, create new data sets after your network’s upgrade, then reuse them. Existing data sets remain supported, but their piece records keep the old storage format permanently. Neither the contract upgrade nor the SDK moves existing content automatically.
What changed?
Section titled “What changed?”| Change | Benefit for your application |
|---|---|
| Compact piece storage: each piece uses two storage slots instead of five. | Less on-chain state to write and read, reducing gas for additions, proofs, and removal. Available only in new data sets. |
| Smaller, split addition events: large additions can emit several compact events. | Removes the old 41-piece event ceiling, allowing more pieces in one transaction. Applies to old and new data sets after upgrade. |
| Lighter piece metadata: application metadata is signed and emitted in events, but no longer written to contract storage. Batches without metadata also use a smaller request encoding. | Fewer storage writes; omitting piece metadata saves another 128 bytes per piece in the request, leaving room for more pieces. Applies to new additions in old and new data sets. |
Your files still have their own PieceCIDs and remain individually retrievable. Batching combines their on-chain registration, not their contents. If your application reads piece metadata, retrieve it from FWSS PieceAdded events or an indexer; the old metadata getters have been removed.
Sources: compact storage, addition events, event-based metadata, smaller metadata-free requests.
More pieces per operation
Section titled “More pieces per operation”Previously, the addition event limited transactions to 41 pieces, and the SDK/provider path used a conservative limit of 40. The upgraded contract splits addition events into groups of 100; 100 is an event size, not a transaction limit. A transaction can contain several such events. Event limits
With the larger event capacity, request size becomes a limit on how many pieces can fit. Less metadata leaves more room for pieces:
| Metadata on each piece | Append to a data set | Create a data set and add pieces |
|---|---|---|
| None | Up to 404 | Up to 401 |
| One pair with a value of 1–32 bytes | Up to 134 | Up to 133 |
| Three pairs with values of 65–96 bytes each | Up to 61 | Up to 60 |
These are request-size estimates, not current upload limits. Creation options and transaction gas can reduce capacity. Metadata keys occupy the same space throughout their allowed 1–32-byte range; value length determines the difference shown here.
Legacy data sets have an 80-piece gas safety cap in addition to the SDK’s encoded message-size check. Compact data sets and new-data-set creation use the same size check without that legacy cap. Provider limits and transaction gas can further reduce batch size. Legacy batching limit
The SDK batches compatible concurrent uploads automatically. Each provider has its own batch, so storing two copies requires separate on-chain operations for the two providers.
Lower cost per piece
Section titled “Lower cost per piece”There are two benefits: providers spend less gas processing pieces, and users pay fewer fixed operation fees when pieces share a transaction.
Less gas for providers
Section titled “Less gas for providers”The compact-storage benchmark measured 14–19% less gas for a single-piece addition and about 85% less gas per piece in a 41-piece addition, compared with the legacy implementation. A separate proving benchmark measured 34% less gas per daily proving cycle for compact data sets. Storage benchmarks, proving benchmark
The event change also demonstrated larger batches: 135 pieces with one metadata pair and 225 without metadata in its benchmark configuration. Those batches reduced gas per piece by a further 16% and 34–35% relative to that benchmark’s smaller batches. Subsequent metadata changes remove more storage and request overhead. These measurements use different intermediate builds and workloads; they are not a single combined savings percentage or current provider limits. Larger-batch benchmarks
Fewer operation fees for users
Section titled “Fewer operation fees for users”At the FWSS 1.4.0 schedule, adding pieces costs 0.008 USDFC per operation plus 0.003 USDFC per piece. For 40 pieces added to one existing data set:
| Submission | Add-pieces fees per copy |
|---|---|
| 40 separate operations | 40 × (0.008 + 0.003) = 0.440 USDFC |
| One operation containing 40 pieces | 0.008 + 40 × 0.003 = 0.128 USDFC |
That is about 71% less in add-pieces fees for the same files. Larger supported batches spread the base fee further. This excludes creation fees, recurring storage, and other charges; each replica pays separately.
Use prepare() to calculate funding for your uploads. See Storage Costs for the current service fees.
Get the full benefit with new data sets
Section titled “Get the full benefit with new data sets”Data sets created after the September 3 upgrade use compact storage. Existing data sets keep their original format, including any pieces added to them later. Switching is opt-in: the SDK does not automatically recreate data sets or move their content. Storage upgrade
Choose new data sets once, then reuse them
Section titled “Choose new data sets once, then reuse them”The SDK prefers matching compact data sets over legacy ones during automatic and explicit provider selection. If no compact data set matches, it can reuse a legacy one. To keep the same metadata while switching, create replacements with the core createDataSet() and waitForCreateDataSet() APIs, then reuse them. Passing dataSetIds explicitly selects those data sets regardless of storage format. Selection does not move existing content.
Alternatively, the following example requests a separate group of data sets by adding a stable metadata value unused by your old data sets. Changing metadata is optional; it is a way to request new data sets with the current high-level API. Keep the value on subsequent uploads to reuse those data sets:
// Preserve your app's other metadata. Keep this generation value for future uploads.
const const metadata: {
app: string;
storageGeneration: string;
}
metadata = { app: stringapp: 'my-app', storageGeneration: stringstorageGeneration: 'compact-2026-09' }
const const contexts: StorageContext[]contexts = await const synapse: Synapsesynapse.Synapse.storage: StorageManagerstorage.StorageManager.createContexts(options?: CreateContextsOptions): Promise<StorageContext[]>createContexts({ CreateContextsOptions.copies?: number | undefinedcopies: 2, BaseContextOptions.metadata?: Record<string, string> | undefinedmetadata })
const const data: Uint8Array<ArrayBuffer>data = new var TextEncoder: new () => TextEncoderThe **`TextEncoder`** interface enables you to encode a JavaScript string using UTF-8.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder)TextEncoder().TextEncoder.encode(input?: string): Uint8Array<ArrayBuffer>The **`TextEncoder.encode()`** method takes a string as input, and returns a Uint8Array containing the string encoded using UTF-8.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/TextEncoder/encode)encode('Hello, compact storage!'.String.repeat(count: number): stringReturns a String value that is made from count copies appended together. If count is 0,
the empty string is returned.repeat(8))
const const prep: PrepareResultprep = await const synapse: Synapsesynapse.Synapse.storage: StorageManagerstorage.StorageManager.prepare(options: PrepareOptions): Promise<PrepareResult>prepare({
PrepareOptions.context?: StorageContext | StorageContext[] | undefinedcontext: const contexts: StorageContext[]contexts,
PrepareOptions.pieceSizes: readonly bigint[]pieceSizes: [var BigInt: BigIntConstructor
(value: bigint | boolean | number | string) => bigint
BigInt(const data: Uint8Array<ArrayBuffer>data.Uint8Array<ArrayBuffer>.byteLength: numberThe length in bytes of the array.byteLength)],
})
if (const prep: PrepareResultprep.PrepareResult.transaction: {
depositAmount: bigint;
includesApproval: boolean;
execute: (options?: {
onHash?: (hash: Hash) => void;
}) => Promise<{
hash: Hash;
receipt: import("viem").TransactionReceipt | null;
}>;
} | null
transaction) await const prep: PrepareResultprep.PrepareResult.transaction: {
depositAmount: bigint;
includesApproval: boolean;
execute: (options?: {
onHash?: (hash: Hash) => void;
}) => Promise<{
hash: Hash;
receipt: import("viem").TransactionReceipt | null;
}>;
}
transaction.execute: (options?: {
onHash?: (hash: Hash) => void;
}) => Promise<{
hash: Hash;
receipt: import("viem").TransactionReceipt | null;
}>
execute()
const const result: UploadResultresult = await const synapse: Synapsesynapse.Synapse.storage: StorageManagerstorage.StorageManager.upload(data: UploadPieceStreamingData, options?: StorageManagerUploadOptions): Promise<UploadResult>upload(const data: Uint8Array<ArrayBuffer>data, { StorageManagerUploadOptions.contexts?: StorageContext[] | undefinedcontexts })
if (const result: UploadResultresult.UploadResult.complete: booleancomplete === false) throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error('Not all requested copies were committed')
for (const const copy: CopyResultcopy of const result: UploadResultresult.UploadResult.copies: CopyResult[]copies) {
var console: Consoleconsole.Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(const copy: CopyResultcopy.CopyResult.providerId: bigintproviderId, const copy: CopyResultcopy.CopyResult.dataSetId: bigintdataSetId, const copy: CopyResultcopy.CopyResult.pieceId: bigintpieceId)
}New data sets are created at the first commit. Save the returned data-set IDs, or keep using the same metadata, source, and CDN setting to reuse them. Stop passing old dataSetIds or old contexts. Creating replacement data sets incurs creation fees and new reserves, so reuse the replacements rather than creating one per upload. Data-set matching
Move existing content only when useful
Section titled “Move existing content only when useful”You can leave existing content where it is and use compact data sets only for future uploads. Adding more pieces to an old data set does not change its format.
To move existing content, upload the same bytes or pull and commit into the new data sets. Confirm all required copies and retrieval before removing old pieces or terminating the old service. PieceCIDs stay the same for unchanged content; record the new data-set and piece IDs and preserve application metadata. Old and new copies both incur charges while active, and old lockups follow the normal service lifecycle.
For applications consuming contract events directly, support PDPVerifier PiecesAddedV2 as well as historical PiecesAdded logs. FWSS PieceAdded, which carries application metadata, is a separate event. See the migration guide for SDK API changes.