Skip to content

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.

ChangeBenefit 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.

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 pieceAppend to a data setCreate a data set and add pieces
NoneUp to 404Up to 401
One pair with a value of 1–32 bytesUp to 134Up to 133
Three pairs with values of 65–96 bytes eachUp to 61Up 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.

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.

There are two benefits: providers spend less gas processing pieces, and users pay fewer fixed operation fees when pieces share a transaction.

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

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:

SubmissionAdd-pieces fees per copy
40 separate operations40 × (0.008 + 0.003) = 0.440 USDFC
One operation containing 40 pieces0.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.

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 matches data sets by metadata and may keep reusing an old one after you upgrade. If you want to keep the same metadata, explicitly create replacements with the core createDataSet() and waitForCreateDataSet() APIs, then select their returned IDs with dataSetIds for future uploads. Preferential selection of already-created compact data sets is tracked separately in #925; it 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: string
app
: 'my-app',
storageGeneration: string
storageGeneration
: 'compact-2026-09' }
const
const contexts: StorageContext[]
contexts
= await
const synapse: Synapse
synapse
.
Synapse.storage: StorageManager
storage
.
StorageManager.createContexts(options?: CreateContextsOptions): Promise<StorageContext[]>
createContexts
({
CreateContextsOptions.copies?: number | undefined
copies
: 2,
BaseContextOptions.metadata?: Record<string, string> | undefined
metadata
})
const
const data: Uint8Array<ArrayBuffer>
data
= new
var TextEncoder: new () => TextEncoder

The TextEncoder interface enables you to encode a JavaScript string using UTF-8.

MDN Reference

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

encode
('Hello, compact storage!'.
String.repeat(count: number): string

Returns a String value that is made from count copies appended together. If count is 0, the empty string is returned.

@paramcount number of copies to append

repeat
(8))
const
const prep: PrepareResult
prep
= await
const synapse: Synapse
synapse
.
Synapse.storage: StorageManager
storage
.
StorageManager.prepare(options: PrepareOptions): Promise<PrepareResult>
prepare
({
PrepareOptions.context?: StorageContext | StorageContext[] | undefined
context
:
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: number

The length in bytes of the array.

byteLength
)],
})
if (
const prep: PrepareResult
prep
.
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: PrepareResult
prep
.
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: UploadResult
result
= await
const synapse: Synapse
synapse
.
Synapse.storage: StorageManager
storage
.
StorageManager.upload(data: UploadPieceStreamingData, options?: StorageManagerUploadOptions): Promise<UploadResult>
upload
(
const data: Uint8Array<ArrayBuffer>
data
, {
StorageManagerUploadOptions.contexts?: StorageContext[] | undefined
contexts
})
if (
const result: UploadResult
result
.
UploadResult.complete: boolean
complete
=== 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: CopyResult
copy
of
const result: UploadResult
result
.
UploadResult.copies: CopyResult[]
copies
) {
var console: Console
console
.
Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const copy: CopyResult
copy
.
CopyResult.providerId: bigint
providerId
,
const copy: CopyResult
copy
.
CopyResult.dataSetId: bigint
dataSetId
,
const copy: CopyResult
copy
.
CopyResult.pieceId: bigint
pieceId
)
}

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

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.