API Reference
Overview
Buffer encodes almost every Roblox/Luau value type into a compact binary format with type tags. It features multi-tiered LRU caching, schema validation, delta encoding, compression, and async processing for large data structures.
Notice
All methods that return (buffer?, string?) return an error message as the second return value on failure. Use SafeDecode to handle untrusted input without throwing.
Warning
The buffer returned from Encode is immutable for the purpose of caching. Do not modify it directly.
Failure
Encoding nil as a root value is not supported and will return an error. To encode a "null" value, wrap it in a table (e.g., {value = nil}).
Core API
Buffer.Encode
Buffer.Encode(value: any) -> (buffer?, errorMsg: string?)
Encodes any supported value into a binary buffer. Non-table results (booleans, numbers, strings under 1000 bytes) are automatically cached in the "warm" LRU tier.
Returns:
On success: buffer — the encoded binary data;
On failure: nil, errorMsg — descriptive error message
Example:
local buffer, err = Buffer.Encode({ player = "John", score = 1000 })
if not buffer then
warn("Encoding failed:", err)
return
end
Warning
nil as a root value is not supported. To encode a null, wrap it in a table: { value = nil }.
Notice
Tables are not automatically cached due to mutability. Use EncodeWithPolicy for table caching.
Buffer.Decode
Buffer.Decode(b: buffer) -> any
Decodes a buffer back into a value. Throws an error if the buffer is corrupt or malformed.
Parameters:
b: buffer — the encoded binary data
Returns: The decoded value
Example:
local value = Buffer.Decode(buffer)
print(value.player, value.score) -- "John", 1000
Failure
This method throws on error. Always use SafeDecode for untrusted input.
Buffer.SafeDecode
Buffer.SafeDecode(b: buffer) -> (value: any, errorMsg: string?)
Decodes a buffer safely, returning an error message instead of throwing. Ideal for handling network data, file input, or external sources.
Parameters:
b: buffer — the encoded binary data
Returns:
On success: value, nil;
On failure: nil, errorMsg
Example:
local value, err = Buffer.SafeDecode(untrustedBuffer)
if err then
print("Corrupt data:", err)
return
end
Notice
Even with SafeDecode, malformed data can cause the decoder to return unexpected but valid Lua values. Always validate the structure after decoding.
Buffer.EncodeSafe
Buffer.EncodeSafe(value: any) -> (buffer?, string?)
A wrapper around Buffer.Encode that performs cycle detection on tables. Prevents infinite recursion from self-referential structures.
Example:
local cycle = {}
cycle.self = cycle -- Cyclic reference
local buffer, err = Buffer.EncodeSafe(cycle)
if not buffer then
print("Cannot encode cycles:", err) -- "cyclic reference at root.self"
end
Failure
Standard Buffer.Encode will crash on cyclic tables. Always use EncodeSafe when the table structure is unknown or untrusted.
Buffer.EncodeWith
Buffer.EncodeWith(value: any, priority: "hot" | "warm" | "cold") -> (buffer?, string?)
Encodes a value with a specific cache tier priority. Overrides the default caching behavior.
Cache Tiers:
| Priority | Capacity | Best For |
|---|---|---|
"hot" |
100 | Values that change every frame (player positions, current state) |
"warm" |
500 | Default tier; general-purpose caching |
"cold" |
2000 | Large, infrequently accessed values (static data, configuration) |
Example:
-- Player position changes every frame — use hot cache
local posBuffer = Buffer.EncodeWith(player.Position, "hot") -- EXAMPLE!!!
-- Game configuration rarely changes — use cold cache
local configBuffer = Buffer.EncodeWith(gameConfig, "cold")
Warning
Cache is keyed by value equality, not reference. Two different tables with identical contents will share a cache entry.
Buffer.EncodeWithPolicy
Buffer.EncodeWithPolicy(value: any, policy: "never" | "always" | "if-shallow") -> (buffer?, string?)
Applies a caching policy specifically for table values. This is the only way to cache tables.
Policies:
"never" — Do not cache the table (default behavior)
"always" — Cache based on a fast content hash of the table's entire contents
"if-shallow" — Cache only if the table contains no nested tables (shallow tables only)
Important:
"always" computes a hash of all key-value pairs. This is safe only for immutable tables (tables that never change).
"if-shallow" is ideal for configuration tables, settings, or any table without nested structures.
Example:
-- Immutable static data — safe to always cache
local staticData = { version = 2, type = "config" }
local buffer1 = Buffer.EncodeWithPolicy(staticData, "always")
-- Nested table — "if-shallow" won't cache this
local nested = { player = { name = "John" } }
local buffer2 = Buffer.EncodeWithPolicy(nested, "if-shallow") -- No caching
-- Shallow table — will be cached
local shallow = { x = 10, y = 20, z = 30 }
local buffer3 = Buffer.EncodeWithPolicy(shallow, "if-shallow") -- Cached
Failure
Using "always" on mutable tables can lead to stale cache entries. Only use this policy for tables that never change after encoding.
Schema System
Schemas provide type safety and structure validation for your data. They're ideal for network protocols, configuration files, and any data that must adhere to a known format. It takes up as little space as possible
Schema Definition
A schema is a table with a version (optional, 1-255) and a fields array:
type Schema = {
version?: number,
fields: {
name: string,
type: string | SchemaType,
optional?: boolean,
default?: any,
}[]
}
Example Schema:
local playerSchema = {
version = 1,
fields = {
{ name = "name", type = "string" },
{ name = "level", type = "number", optional = false },
{ name = "inventory", type = Buffer.Types.array(Buffer.Types.string()) },
{ name = "lastLogin", type = "DateTime", optional = true, default = DateTime.now() }
}
}
Buffer.EncodeSchema
Buffer.EncodeSchema(value: { [string]: any }, schema: Schema) -> (buffer?, string?)
Encodes a value according to a schema, performing validation before encoding.
Validations performed:
Required fields exist
Field types match schema definition
Custom validators (if using Buffer.Types) pass
Example:
local playerData = {
name = "John",
level = 42,
inventory = { "sword", "shield" }
}
local buffer, err = Buffer.EncodeSchema(playerData, playerSchema)
if not buffer then
print("Validation failed:", err)
end
Buffer.DecodeSchema
Buffer.DecodeSchema(b: buffer, schema: Schema) -> ({ [string]: any }?, string?)
Decodes a buffer according to a schema. Returns a table with all fields populated, using defaults for optional fields if they're missing from the encoded data.
Behavior:
Fields not present in the buffer use the schema's default value (if specified)
Missing required fields return an error
Type validation is performed on all decoded values
Example:
local data, err = Buffer.DecodeSchema(buffer, playerSchema)
if not err then
print(data.name, data.level) -- "John", 42
print(data.lastLogin) -- DateTime.now() (default)
end
Buffer.ValidateSchema
Buffer.ValidateSchema(schema: Schema) -> (boolean, string?)
Validates a schema definition without encoding any data.
Validates:
fields is an array
Each field has a non-empty name
Field names are unique
Field type is a valid string or SchemaType object
Optional fields with defaults are properly marked
Example:
local ok, err = Buffer.ValidateSchema(playerSchema)
if not ok then
error("Invalid schema: " .. err)
end
Failure
A schema must have at least one field. Empty schemas are rejected.
Buffer.Types
Buffer.Types provides factory functions for creating type validators used in schemas.
Types.any()
Buffer.Types.any() -> SchemaType
Accepts any value. Useful for polymorphic fields.
Types.exact()
Buffer.Types.exact(typeofStr: string) -> SchemaType
Requires the value to have exactly the given typeof() result.
Types.number()
Buffer.Types.number() -> SchemaType
Accepts any Lua number.
Types.numberRange()
Buffer.Types.numberRange(min: number, max: number) -> SchemaType
Accepts numbers within the inclusive range [min, max].
Types.integer()
Buffer.Types.integer() -> SchemaType
Accepts whole numbers (no fractional part).
Types.string()
Buffer.Types.string(maxLen: number?) -> SchemaType
Accepts strings. Optionally enforces a maximum length.
Types.boolean()
Buffer.Types.boolean() -> SchemaType
Accepts boolean values.
Types.array()
Buffer.Types.array(elementType: SchemaType, maxLen: number?) -> SchemaType
Accepts Lua sequences (contiguous integer keys starting from 1). Validates each element against elementType. Optionally enforces maximum length.
Example:
local stringArray = Buffer.Types.array(Buffer.Types.string())
local numberArray = Buffer.Types.array(Buffer.Types.numberRange(0, 100), 10)
Types.union()
Buffer.Types.union(...: SchemaType) -> SchemaType
Accepts values that match any of the provided types.
Example:
local stringOrNumber = Buffer.Types.union(
Buffer.Types.string(),
Buffer.Types.number()
)
Types.optional()
Buffer.Types.optional(inner: SchemaType) -> SchemaType
Accepts either nil or a value matching the inner type.
Types.literal()
Buffer.Types.literal(...: any) -> SchemaType
Accepts only the exact literal values provided.
Example:
local direction = Buffer.Types.literal("up", "down", "left", "right")
Schema Deltas
Deltas encode only the differences between two schema-compliant tables, significantly reducing storage and transmission size for incremental updates.
Buffer.EncodeSchemaDelta
Buffer.EncodeSchemaDelta(
old: { [string]: any },
new: { [string]: any },
schema: Schema
) -> (buffer?, string?)
Encodes only the fields that differ between old and new. Uses a bitmask to indicate which fields changed.
Requirements:
Both old and new must be valid against the schema
Fields with nil in new are considered unchanged (not encoded)
Example:
local old = { name = "John", level = 42, score = 1000 }
local new = { name = "John", level = 43, score = 1000 }
-- Only encodes the 'level' field (changed from 42 to 43)
local delta, err = Buffer.EncodeSchemaDelta(old, new, playerSchema)
Notice
The encoded delta is typically much smaller than re-encoding the entire new table, especially when few fields change.
Buffer.DecodeSchemaDelta
Buffer.DecodeSchemaDelta(
b: buffer,
current: { [string]: any },
schema: Schema
) -> ({ [string]: any }?, string?)
Applies a delta buffer to the current table, updating it with the changes. The table is modified in-place.
Example:
-- current is { name = "John", level = 42, score = 1000 }
local updated, err = Buffer.DecodeSchemaDelta(deltaBuffer, current, playerSchema)
-- updated.level is now 43
Schema Migration
When your schema evolves over time, migrations allow you to transform older data versions to the latest format.
MigrationMap
type MigrationMap = { [fromVersion: number]: (data: { [string]: any }) -> { [string]: any } }
A table mapping a version number to a migration function. The function receives data in the old format and returns data in the new format for the next version.
Example:
local migrations = {
[1] = function(data)
-- Version 1 → Version 2: Add default inventory
data.inventory = data.inventory or {}
return data
end,
[2] = function(data)
-- Version 2 → Version 3: Rename 'xp' to 'experience'
data.experience = data.xp
data.xp = nil
return data
end
}
Buffer.MigrateSchema
Buffer.MigrateSchema(
data: { [string]: any },
fromVersion: number,
toVersion: number,
migrations: MigrationMap
) -> ({ [string]: any }?, string?)
Applies the necessary migration steps to transform data from fromVersion to toVersion. The original data is not modified.
Returns:
On success: the migrated data table
On failure: nil, errorMsg
Example:
local oldData = { name = "John", xp = 100 } -- Version 1
local migrated, err = Buffer.MigrateSchema(oldData, 1, 3, migrations)
-- migrated now has: { name = "John", experience = 100, inventory = {} }
Warning
Migrations are applied sequentially. You cannot skip versions or downgrade (fromVersion must be less than toVersion).
Compression
Buffer supports transparent compression using LZ4, Deflate, and Zstd algorithms.
Buffer.EncodeCompressed
Buffer.EncodeCompressed(
value: any,
mode: CompressionMode?,
hint: string?
) -> (buffer?, string?)
Encodes a value and compresses the resulting buffer. The compression method is stored as a tag in the output, allowing Buffer.Decode to decompress automatically.
Compression Modes:
| Mode | Description |
|---|---|
"auto" |
Automatically selects the best algorithm based on data size and entropy (default) |
"lz4" |
Fast compression/decompression, moderate ratio |
"deflate" |
Balanced compression, widely compatible |
"zstd" |
High compression ratio, good speed |
Hints:
"binary" — Data is binary (e.g., images, custom formats)
"text" — Data is human-readable text
"json" — Data is JSON-like
Example:
-- Auto-select best compression
local compressed, err = Buffer.EncodeCompressed(hugeTable)
-- Force Zstd with binary hint
local compressed, err = Buffer.EncodeCompressed(largeData, "zstd", "binary")
Notice
The output buffer can be passed directly to Buffer.Decode — decompression happens automatically.
Buffer.SetCompressionDict
Buffer.SetCompressionDict(dict: buffer)
Sets a custom dictionary for LZ4 and Zstd compression. Dictionaries can significantly improve compression ratios for small, repetitive data by providing a common pattern base.
Use cases: Game state snapshots with known structure Network packets with repeated headers Configuration files with common keys
Example:
local dict = Buffer.Encode({
{ "position", "velocity", "rotation" }, -- Common field names
{ "player", "npc", "item" } -- Common object types
})
Buffer.SetCompressionDict(dict)
Warning
The dictionary must be identical for compression and decompression. Include it in your game's assets or transmit it with the data.
Instance Registry
Roblox Instances are encoded as numeric IDs (u32) rather than full objects. This requires a registry to map IDs to instances.
Buffer.RegisterInstance
Buffer.RegisterInstance(instance: Instance, id: number)
Registers an instance with a numeric ID. The decoder uses the same ID to look up the instance.
Requirements: ID must be between 0 and 0xFFFFFFFF (u32 range) Each instance can have only one ID IDs must be unique
Example:
local player = game.Players.LocalPlayer
Buffer.RegisterInstance(player, 1)
-- Now player can be encoded and decoded
local buffer = Buffer.Encode(player)
local decoded = Buffer.Decode(buffer) -- Returns the same player instance
Notice
The library automatically unregisters instances when they're destroyed (using the Destroying event). No manual cleanup is needed.
Buffer.UnregisterInstance
Buffer.UnregisterInstance(instance: Instance)
Manually unregisters an instance. This is automatically handled by the library, but can be used for explicit cleanup.
Custom Types
You can extend Buffer to support custom Roblox or Lua types by registering a custom encoder/decoder.
CustomType Definition
type CustomType = {
encode: (BB: buffer, value: any, offset: number) -> (number?, string?),
decode: (b: buffer, offset: number) -> (any, number),
}
- encode: Writes the value to the buffer at
offset. Returns the new offset ornil, error. - decode: Reads a value from the buffer at
offset. Returns(value, newOffset).
Buffer.RegisterType
Buffer.RegisterType(id: number, ct: CustomType)
Registers a custom type with a numeric ID (0-254). IDs 0-127 are reserved for future library use.
Example:
-- Custom type for UDim2 (though already supported)
Buffer.RegisterType(200, {
encode = function(BB, value, offset)
buf_writeu8(BB, offset, T_UDIM2)
buf_writef32(BB, offset+1, value.X.Scale)
buf_writei32(BB, offset+5, value.X.Offset)
buf_writef32(BB, offset+9, value.Y.Scale)
buf_writei32(BB, offset+13, value.Y.Offset)
return offset + 17, nil
end,
decode = function(b, offset)
return UDim2.new(
buf_readf32(b, offset),
buf_readi32(b, offset+4),
buf_readf32(b, offset+8),
buf_readi32(b, offset+12)
), offset + 16
end
})
Warning
Custom type IDs must be unique. Registering with an existing ID will overwrite it.
Advanced Encoding
Buffer.EncodeDelta
Buffer.EncodeDelta(old: { [string]: any }, new: { [string]: any }) -> (buffer?, string?)
Encodes a delta between two dictionaries (not schema-validated). Fields present in old but missing in new are encoded as nil (deleted). This is a lower-level API than schema deltas.
Example:
local old = { a = 1, b = 2, c = 3 }
local new = { a = 1, b = 4 } -- c is deleted
local delta = Buffer.EncodeDelta(old, new)
-- delta encodes: b changed to 4, c deleted (nil)
Buffer.ApplyDelta
Buffer.ApplyDelta(target: { [string]: any }, delta: buffer) -> { [string]: any }
Applies a delta buffer to a target table, modifying it in-place. Returns the target for chaining.
Example:
local target = { a = 1, b = 2, c = 3 }
Buffer.ApplyDelta(target, deltaBuffer)
-- target is now { a = 1, b = 4 } (c removed)
Buffer.CreateStream
Buffer.CreateStream() -> Stream
Creates a streaming encoder for writing multiple values sequentially. Useful for batched operations or when you don't know the final size ahead of time.
Stream Methods:
- stream:write(value: any) -> (boolean, string?) — Encode and append a value
- stream:finalize() -> (buffer?, string?) — Return the concatenated buffer
Example:
local stream = Buffer.CreateStream()
stream:write({ type = "player", name = "John" })
stream:write({ type = "score", value = 1000 })
stream:write({ type = "inventory", items = { "sword" } })
local buffer = stream:finalize() -- All three values concatenated
Notice
Once finalized, the stream cannot be written to again.
Asynchronous Operations
For large tables with thousands of fields, synchronous encoding can block the main thread. Async methods process data in chunks, yielding between batches to keep the game responsive.
AsyncHandle
Async methods return a handle with the following methods:
handle:Await() -> (buffer?, string?)— Waits for completion and returns resulthandle:Cancel()— Cancels the ongoing operation
Buffer.EncodeAsync
Buffer.EncodeAsync(value: any) -> AsyncHandle
Encodes a large table asynchronously. Processes fields in chunks of ASYNC_FIELDS_PER_STEP (default 256), yielding between chunks.
Example:
local handle = Buffer.EncodeAsync(largeTable)
-- Do other work while encoding runs...
updateGameState()
-- Wait for completion when needed
local buffer, err = handle:Await()
if buffer then
saveToDataStore(buffer)
end
Notice
Async encoding only yields for tables with more than 256 fields. Small values encode synchronously.
Buffer.DecodeAsync
Buffer.DecodeAsync(b: buffer) -> AsyncHandle
Decodes a buffer asynchronously. Currently processes the entire decode in a single chunk but yields at least once.
Example:
local handle = Buffer.DecodeAsync(buffer)
-- Wait for result
local value, err = handle:Await()
if value then
print("Decoded:", value)
end
Warning
Cancelling an async operation after it completes has no effect. Cancelling during processing prevents the completion callback.
Conversions
Buffer.EncodeToBase64
Buffer.EncodeToBase64(value: any, urlSafe: boolean?) -> (string?, string?)
Encodes a value and converts the binary buffer to a Base64 string. If urlSafe is true, uses URL-safe Base64 encoding (replaces + and / with - and _).
Example:
local b64, err = Buffer.EncodeToBase64({ data = "hello" }, true)
if b64 then
print(b64) -- "eyJkYXRhIjoiaGVsbG8ifQ==" (or URL-safe variant)
end
Buffer.DecodeFromBase64
Buffer.DecodeFromBase64(s: string) -> (any, string?)
Decodes a Base64 string back into a value.
Example:
local value, err = Buffer.DecodeFromBase64(b64String)
if value then
print(value.data) -- "hello"
end
Buffer.BufferToBase64
Buffer.BufferToBase64(b: buffer, urlSafe: boolean?) -> string
Converts a binary buffer to a Base64 string. Throws on error.
Buffer.Base64ToBuffer
Buffer.Base64ToBuffer(s: string) -> (buffer?, string?)
Converts a Base64 string back into a binary buffer. Returns an error on invalid Base64.
Buffer.ToJSON
Buffer.ToJSON(b: buffer) -> (string?, string?)
Decodes a buffer and converts the result to a JSON string. Handles special types (Vector3, CFrame, etc.) by adding a __type field.
Example:
local json, err = Buffer.ToJSON(buffer)
if json then
print(json) -- {"__type":"Vector3","x":1,"y":2,"z":3}
end
Buffer.FromJSON
Buffer.FromJSON(json: string) -> (buffer?, string?)
Parses a JSON string and encodes the result. The JSON must represent a valid Lua value (no functions, userdata, etc.).
Example:
local buffer, err = Buffer.FromJSON('{"player":"John","score":1000}')
if buffer then
-- buffer can now be decoded to a Lua table
end
Debugging & Introspection
Buffer.Visualize
Buffer.Visualize(b: buffer) -> string
Returns a human-readable representation of the buffer's internal structure, showing byte offsets and tag names.
Example output:
[0000] 11 STR16
[0001] 05
[0006] 18 ARRAY
[0007] 03
[0010] 02 U8
[0011] 42
[0012] 02 U8
[0013] 43
[0014] 02 U8
[0015] 44
Buffer.VisualizeEntropy
Buffer.VisualizeEntropy(b: buffer) -> string
Shows the entropy distribution of a buffer, helping to choose the optimal compression algorithm.
Buffer.DetectDataClass
Buffer.DetectDataClass(b: buffer) -> string
Attempts to classify the type of data in a buffer. Returns one of:
- "binary" — Unknown binary data
- "text" — Human-readable text
- "json" — JSON-like structure
- "lz4" — Already LZ4 compressed
- "zstd" — Already Zstd compressed
- "deflate" — Already Deflate compressed
Buffer.Invalidate
Buffer.Invalidate(value: any)
Removes a value from all cache tiers. Useful when you know a cached value is stale.
Example:
local config = loadConfig()
Buffer.EncodeWithPolicy(config, "always") -- Cached
-- ... later, config changes ...
Buffer.Invalidate(config) -- Clear stale cache
Buffer.EncodedSize
Buffer.EncodedSize(value: any) -> (number?, string?)
Estimates the byte size of the encoded value without allocating a final buffer. Useful for pre-allocation, validation, or checking if a value will exceed size limits.
Returns:
On success: number — estimated size in bytes
On failure: nil, errorMsg
Example:
local size, err = Buffer.EncodedSize(hugeTable)
if size and size > 1024 * 1024 then
warn("Table will be 1MB+ after encoding!")
end
Notice
This is an estimate; the actual encoded size may differ slightly due to tag overhead. The estimate is accurate within a few bytes.
Utility Functions
Buffer.Types
The Buffer.Types namespace contains all type factory functions. This is a reference to the Types module, accessible for convenience.
Buffer.CustomByType
Buffer.CustomByType: { [string]: { id: number, ct: CustomType } }
A table mapping type names to registered custom types. Useful for introspection.
manee was here :)