Compression
Esta página aún no está disponible en tu idioma.
Object storage supports at-rest compression — bodies are compressed before put, decompressed on get. Three modes; pick the CPU-vs-size trade that fits the payload.
import { ObjectStorageDurableStateStore, ObjectStorageDurableStateStoreOptions, S3ObjectStorageBackend, S3ObjectStorageOptions,} from 'actor-ts/persistence';
const objectStorageDurableStateStoreOptions = ObjectStorageDurableStateStoreOptions.create() .withBackend(new S3ObjectStorageBackend(S3ObjectStorageOptions.create() /* .withRegion(...).withBucket(...) */)) .withCompression({ algorithm: 'zstd', level: 6, // optional; zstd 1–22 (default 3) });const store = new ObjectStorageDurableStateStore(objectStorageDurableStateStoreOptions);Now every persisted state is zstd-compressed before upload. Reads transparently decompress.
Algorithms
Section titled “Algorithms”| Algorithm | Compression ratio | CPU cost | When |
|---|---|---|---|
zstd | Best | Low–moderate (decode very fast) | Large state blobs — best ratio at competitive speed. |
gzip | Good (typical 50-70 %) | Moderate | Universal default — node:zlib, works on every runtime. |
none | None | Zero | Already-compressed or tiny payloads (see caveat below). |
The store default is gzip — it needs no native runtime
support and no extra dependency. For text-heavy state
(JSON), both gzip and zstd reach ~70 % reduction; zstd gets there
faster and decompresses quicker. For already-compressed data
(encrypted bytes, image data), compression is nearly free of
value and wastes CPU — use none.
There is no
brotliordeflatemode — onlynone,gzip, andzstd.
Configuration
Section titled “Configuration”type CompressionAlgo = 'none' | 'gzip' | 'zstd';
type CompressionConfig = { algorithm: CompressionAlgo; level?: number; // algorithm-specific; clamped; undefined = default};Levels are algorithm-specific. Out-of-range values are
clamped; omitting level uses the implementation default:
- gzip: 0 (no compression) – 9 (best). Default 6.
- zstd: 1 (fastest) – 22 (best). Default 3.
- none: ignored.
For most workloads the defaults are fine — bump only when storage cost dominates.
Runtime support
Section titled “Runtime support”zstd support differs by direction:
- Compress (write) — native only: Bun (
Bun.zstdCompressSync) or Node (zlib.zstdCompressSync). There is no pure-JS fallback for writing; the optionalfzstdpeer dependency is decompress-only. Selectingzstdon a runtime without native support fails fast — eagerly at plugin registration, not cryptically on first persist. - Decompress (read) —
node:zlibfirst (it is the only zstd implementation that takes an allocation-time output bound — see Decompression cap), then Bun’s global, then the optionalfzstdpeer-dep, so a runtime without native zstd can still read zstd bodies written elsewhere oncefzstdis installed.
Both directions pick an implementation by calling it, not by
checking that the function exists — the two differ on Deno, whose
node:zlib exports zstd entry points with no binding behind them.
So a runtime without working zstd gets the framework’s own message
naming the missing capability, and the fzstd fallback below it
stays reachable.
gzip uses node:zlib and works everywhere (Bun, Node, Deno) with
no extra dependency.
How it works
Section titled “How it works”On put: serialize value → JSON bytes → compress(bytes) → [encrypt] → frame with ATS1 manifest → backend.put(framed)
On get: backend.get → framed bytes → read ATS1 manifest → [decrypt] → decompress per the manifest's algorithm flag → JSON → deserializeEvery body carries a small ATS1 manifest header; a 2-bit
field in its flags byte records the compression algorithm
(none / gzip / zstd). Decompression is driven entirely by
that flag — not by any HTTP header. (On S3 the framework also
sets Content-Encoding as a courtesy marker, but it is never the
source of truth for decoding.)
Because each body self-describes its algorithm, mixing none /
gzip / zstd bodies in the same bucket works — every object
decodes per its own manifest.
Decompression cap (bomb protection)
Section titled “Decompression cap (bomb protection)”Decompression on read is bounded to 512 MiB of output by default, so a tampered or hostile compressed body (a few KB that inflates to many GB) can’t exhaust memory during recovery (security audit #3).
gzip and zstd hand the bound to the decoder via zlib’s
maxOutputLength, so an over-cap body is refused before its output is
allocated — the bomb never gets the memory it was built to claim. Every
path additionally asserts the decoded size, which is what bounds the two
that cannot take the option (Bun’s Bun.zstdDecompressSync, reached only
on a Bun without zstd in node:zlib, and the fzstd fallback). There the
cap is still enforced, but only after the output exists — so on a runtime
that falls back to either, treat the bound as a correctness check rather
than as bomb protection.
Either way the read fails with an error instead of OOM-ing the process, and
the error names the bound (maxOutputBytes=…) so it is clear which setting
to reach for. Snapshots / durable state are
far smaller than this in practice; if you legitimately store larger bodies,
raise the bound with withMaxDecompressedBytes on the store (or plugin)
options:
const objectStorageDurableStateStoreOptions = ObjectStorageDurableStateStoreOptions.create() .withBackend(backend) .withMaxDecompressedBytes(2 * 1024 * 1024 * 1024); // 2 GiB; Infinity opts outwithMaxDecompressedBytes is available on the snapshot-store, durable-state-store,
and plugin options; the value is validated at construction (a non-positive /
non-integer cap throws OptionsError).
Changing the level
Section titled “Changing the level”The level is an encoder-only setting. It is not stored on the wire (the manifest records the algorithm, not the level), and the decompressor never needs it — gzip and zstd frames are self-describing.
So changing the configured level requires no migration: bodies written at the old level keep decoding, new bodies use the new level, and the two mix freely. You can also raise or lower the level per-actor at any time without touching existing data.
Compression + serialization
Section titled “Compression + serialization”serialize → JSON bytes → compress → [encrypt] → storeCompression runs after serialization and before encryption
— compressing ciphertext is pointless (it’s high-entropy), and the
fixed order also avoids CRIME-style side channels. For maximum
size reduction on large text-heavy state, zstd at a higher level
is usually the best single knob to turn.
When to use each
Section titled “When to use each”| Scenario | Algorithm |
|---|---|
| Large text-heavy state (big JSON, long descriptions) | zstd |
| Mixed text + numbers | zstd or gzip |
| Maximum portability, no native zstd guaranteed | gzip |
| Already-encrypted / already-compressed bytes | none (no benefit) |
| Storage-cost-bounded, write-once-read-many | zstd (level 15-19) |
| CPU-bounded write path | gzip level 1 or zstd level 1 |
CPU cost (rough guidance)
Section titled “CPU cost (rough guidance)”Single thread, ~100 KB blob — order-of-magnitude only; real numbers depend on payload and runtime:
- gzip level 6 — ~1-3 ms encode, ~0.5 ms decode.
- gzip level 1 — ~0.5 ms encode, fast decode.
- zstd level 3 (default) — encode comparable to gzip, better ratio, very fast decode.
- zstd level 19 — noticeably slower encode, decode still fast.
- zstd levels 20-22 (ultra) — much slower encode for marginal gains; reserve for write-once-read-many bulk data.
zstd’s asymmetry (cheap decode at every level) makes it a strong default for read-heavy state. For typical state-store workloads (small objects, modest write rate) the CPU cost is invisible.
Per-actor override
Section titled “Per-actor override”class SmallStateActor extends DurableStateActor<...> { protected compression() { return { algorithm: 'none' as const }; }}
class LogStateActor extends DurableStateActor<...> { protected compression() { return { algorithm: 'zstd' as const, level: 19 }; }}Override the store-level default per actor. Useful when:
- Most actors have small state (no compression needed).
- A few have large text-heavy state (high compression).
See Per-actor policies.
Where to next
Section titled “Where to next”- Object storage overview — the bigger picture.
- Encryption — the complementary at-rest feature.
- Per-actor policies — configuring compression per-actor.
