VoidSentry

High-performance buffer serialization for Roblox

Purpose

VoidSentry is a powerful, low-level buffer serializer designed for efficient data transmission in Roblox games. It provides both static (schema-based) and dynamic (schemaless) serialization with support for a wide range of data types.

๐Ÿš€ High Performance

Native optimizations and efficient buffer operations for maximum speed

๐Ÿ“ฆ Zero Dependencies

Standalone library with no external requirements

๐ŸŽฏ Type Safe

Full strict-mode Luau type annotations for better IDE support

โšก Flexible

30+ built-in types including primitives, Roblox types, and complex structures

Installation

Wally:

[dependencies]
VoidSentry = "elentium/voidsentry@0.0.7"

Direct(rbxm):

find the VoidSentry.rbxm in roblox-direct file in repo

Then run: wally install

Quick Start

Static Serializer (Recommended)

--!strict
local VoidSentry = require(ReplicatedStorage.Packages.VoidSentry)
local Types = VoidSentry.Types

-- Create a serializer with a fixed schema
local Serializer = VoidSentry.Static.new(
    nil, -- No compression
    Types.Int32,
    Types.String,
    Types.Struct({
        Hello = Types.String,
        World = Types.Int32,
    })
)

-- Serialize data
local b = Serializer:serialize(nil, 42, "Hello, world!", {
    Hello = "hi",
    World = 999,
})

-- Deserialize data
local int, str, struct = Serializer:deserialize(nil, b)

Dynamic Serializer

local VoidSentry = require(ReplicatedStorage.Packages.VoidSentry)
local Dynamic = VoidSentry.Dynamic

-- No schema required!
local buffer = Dynamic.serialize(
    nil, -- No compression
    nil, -- No offset
    42,
    "Dynamic serialization",
    Vector3.new(10, 20, 30),
    true
)

-- Deserialize automatically
local int, str, vec, bool = Dynamic.deserialize(nil, nil, buffer)

Performance Benchmarks

* All benchmarks performed with 2000 iterations per test on standard Roblox hardware. Results show average time per operation.

Static Serializer Performance

Test Case Serialize (avg) Deserialize (avg) Total (avg)
Simple Types (Int32, String, Bool)0.000959 ms0.000443 ms0.001402 ms
Vector Types (Vector3, Vector3F24)0.001292 ms0.000431 ms0.001723 ms
Array Types (Array<Int32>)0.001149 ms0.000563 ms0.001712 ms
Struct Types (PlayerData)0.001086 ms0.000782 ms0.001868 ms
Complex Nested Types0.002128 ms0.001329 ms0.003457 ms
With Compression (Level 5)0.002118 ms0.001450 ms0.003568 ms

Dynamic Serializer Performance

Test Case Serialize (avg) Deserialize (avg) Total (avg)
Simple Types (Int32, String, Bool)0.002071 ms0.003448 ms0.005519 ms
Vector Types (Vector3)0.001214 ms0.001489 ms0.002703 ms
Array Types (Mixed Array)0.012221 ms0.009558 ms0.021779 ms
Struct Types (PlayerData as Table)0.014557 ms0.012684 ms0.027241 ms
Complex Nested Types0.017743 ms0.019277 ms0.037020 ms
Mixed Types (Multiple Types)0.011130 ms0.013152 ms0.024282 ms
With Compression (Level 5)0.003054 ms0.004565 ms0.007619 ms

Performance Comparison

4.59x

faster (Static vs Dynamic)

Simple types benchmark shows Static serializer is ~4.6x faster

Static Simple Types

0.001174 ms

average per operation

Serialize + Deserialize combined

Dynamic Simple Types

0.005392 ms

average per operation

Serialize + Deserialize combined

Overhead

0.004218 ms

additional time per operation

Dynamic adds type inference overhead

๐Ÿ’ก Key Insights

  • Static serializer is significantly faster (4.59x) for simple types
  • Dynamic serializer overhead increases dramatically with complex nested structures
  • Compression adds overhead but can be beneficial for larger datasets
  • Deserialization is generally faster than serialization for Static, but slower for Dynamic due to type inference
  • Use Static serializer when schema is known for optimal performance

API Reference

Static Serializer

VoidSentry.Static.new(compressionLevel: number?, ...TypeNode): StaticObject

Creates a new static serializer with a fixed schema.

Parameters:
  • compressionLevel (optional): Zstd compression level (-7 to 22, or nil for no compression)
  • ...TypeNode: Variable number of type nodes defining the schema
Returns: A StaticObject with serialize and deserialize methods

StaticObject:serialize(offset: number?, ...values): buffer

Serializes data according to the schema.

Parameters:
  • offset (optional): Starting byte offset (reserves offset bytes at the beginning for custom metadata)
  • ...values: Values matching the schema types
Returns: A buffer containing the serialized data

StaticObject:deserialize(offset: number?, buffer: buffer): ...values

Deserializes data from a buffer.

Parameters:
  • offset (optional): Starting byte offset to read from
  • buffer: The buffer to deserialize
Returns: One or multiple values matching the schema

Dynamic Serializer

VoidSentry.Dynamic.serialize(compressionLevel: number?, offset: number?, ...values): buffer

Serializes data with automatic type inference.

Parameters:
  • compressionLevel (optional): Zstd compression level (-7 to 22)
  • offset (optional): Starting byte offset
  • ...values: Any serializable values
Returns: A buffer with type information and data

VoidSentry.Dynamic.deserialize(compressionLevel: number?, offset: number?, buffer: buffer): ...values

Deserializes data with embedded type information.

Parameters:
  • compressionLevel (optional): Must match serialization compression level
  • offset (optional): Starting byte offset
  • buffer: The buffer to deserialize
Returns: All values that were serialized

Available Types

Numeric Types

Type Description Size Range
Types.Int8Signed 8-bit integer1 byte-128 to 127
Types.UInt8Unsigned 8-bit integer1 byte0 to 255
Types.Int16Signed 16-bit integer2 bytes-32,768 to 32,767
Types.UInt16Unsigned 16-bit integer2 bytes0 to 65,535
Types.Int32Signed 32-bit integer4 bytes-2ยณยน to 2ยณยน-1
Types.UInt32Unsigned 32-bit integer4 bytes0 to 2ยณยฒ-1
Types.Float3232-bit floating point4 bytesIEEE 754 single precision
Types.Float6464-bit floating point8 bytesIEEE 754 double precision
Types.Float2424-bit floating point3 bytesReduced precision (custom format)

String Types

Type Description Max Size
Types.StringStandard string65,535 + 2 bytes (16-bit length prefix)
Types.StringTinyCompact string255 + 1 byte (8-bit length prefix)
Types.StringFixed(len)Fixed length stringUser-defined length
Types.StringNullTerminatedNull-terminated string (C-style)Unlimited

Boolean & Special Types

  • Types.Bool - Boolean value (1 byte)
  • Types.Void - Empty table (0 bytes)
  • Types.Nil - Nil value (0 bytes)
  • Types.Any - Any type (dynamic, includes type information)

Roblox Types

Type Description Size
Types.Vector3Full precision Vector312 bytes
Types.Vector3F24Reduced precision Vector39 bytes
Types.Vector3int16Integer Vector36 bytes
Types.Vector2Full precision Vector28 bytes
Types.Vector2F24Reduced precision Vector26 bytes
Types.Vector2int16Integer Vector24 bytes
Types.VectorFull precision vector (luau native vector type)12 bytes
Types.VectorF24Reduced precision vector9 bytes
Types.VectorInt16Integer vector6 bytes
Types.CFrameFull precision CFrame48 bytes
Types.CFrameQQuaternion CFrame28 bytes
Types.Color3RGB color3 bytes
Types.Enum(EnumType)Enum value (requires Enum parameter)2 bytes
Types.InstanceRoblox InstanceVariable(Struct)

Collection Types

Type Description Parameters Max Size
Types.Array(elementType)Variable-size arrayElement type65,535 elements
Types.ArrayTiny(elementType)Compact arrayElement type255 elements
Types.ArrayFixed(elementType, len)Fixed-size arrayElement type, lengthUser-defined
Types.Map(keyType, valueType)Variable-size mapKey type, value type65,535 entries
Types.MapFixed(keyType, valueType, len)Fixed-size mapKey type, value type, lengthUser-defined
Types.Struct({field = Type, ...})Fixed structure with named fieldsSchema objectFixed fields
Types.Optional(type)Nullable typeBase typeSame as base type + 1 byte
Types.BoolPackedArray of exactly 8 booleansNone8 booleans (1 byte)
Types.Bits._8(count)Array of 8-bit valuesCountUser-defined
Types.Bits._16(count)Array of 16-bit valuesCountUser-defined
Types.Bits._32(count)Array of 32-bit valuesCountUser-defined

Instance Type

Types.Instance

Instances are serialized like structs. In order for the serializer to recognize an Instance's ClassName, it must be predefined in serialize_data.luau as a struct schema.

How it works:
  • Each Instance ClassName you want to serialize must have a corresponding struct definition in serialize_data.luau
  • The struct defines which properties of the Instance will be serialized
  • Only the properties defined in the struct schema will be included in serialization
Example:
-- In serialize_data.luau, define the Instance schema:
-- Example for a "Part" class
Part = Types.Struct({
    Name = Types.String,
    Position = Types.Vector3,
    Size = Types.Vector3,
    Color = Types.Color3,
    Anchored = Types.Bool,
})

-- Then use Types.Instance in your serializer:
local Serializer = VoidSentry.Static.new(nil, Types.Instance("Part"))

-- Serialize the Instance
local part = workspace.MyPart
local buffer = Serializer:serialize(nil, part)

๐Ÿ’ก Important

  • The ClassName must exactly match the struct name in serialize_data.luau
  • Undefined ClassNames will cause an error during serialization
  • Only properties defined in the struct will be serialized โ€” other properties are ignored

Best Practices & Warnings

Best Practices

โœ… Use Static Serializer When Possible

The static serializer is significantly faster (3x+) than dynamic serialization because it doesn't need to infer types at runtime. Always use static serialization when you know your data structure ahead of time.

-- โœ… Good: Static serializer
local Serializer = VoidSentry.Static.new(nil, Types.Int32, Types.String)

-- โš ๏ธ Only use dynamic when structure is unknown
local buffer = VoidSentry.Dynamic.serialize(nil, nil, data)

โœ… Choose Appropriate Types

Use the smallest type that fits your data to reduce bandwidth and improve performance.

-- โœ… Good: Use Int16 for small numbers
Types.Int16  -- -32,768 to 32,767

-- โŒ Bad: Using Int32 for small numbers
Types.Int32  -- Unnecessary overhead

-- โœ… Good: Use Vector3F24 for approximate positions
Types.Vector3F24  -- 9 bytes vs 12 bytes

-- โœ… Good: Use StringTiny for short strings
Types.StringTiny  -- For strings โ‰ค 255 characters

โœ… Reuse Serializers

Create serializer objects once and reuse them. Don't create new serializers on every serialization call.

-- โœ… Good: Create once, reuse
local PlayerSerializer = VoidSentry.Static.new(nil, Types.Struct({...}))

for _, player in players do
    local b = PlayerSerializer:serialize(nil, player)
    -- Send buffer
end

-- โŒ Bad: Creating new serializer each time
for _, player in players do
    local serializer = VoidSentry.Static.new(nil, Types.Struct({...}))
    local b = serializer:serialize(nil, player)
end

โœ… Batch Serialization

Serialize multiple values at once rather than separately to reduce overhead.

-- โœ… Good: Serialize together
local b = Serializer:serialize(nil, intValue, stringValue, structValue)

-- โŒ Bad: Multiple serializations
local b1 = Serializer:serialize(nil, intValue)
local b2 = Serializer:serialize(nil, stringValue)
local b3 = Serializer:serialize(nil, structValue)

โœ… Compression Trade-offs

Compression reduces bandwidth but increases CPU usage. Use compression for large data or slow connections.

  • -7 to 3: Fast compression, lower ratio (good for real-time)
  • 5 to 10: Balanced (recommended for most cases)
  • 15 to 22: Maximum compression, slower (good for storage)

โœ… Validate Data Types

Ensure your data matches the expected types before serialization to avoid runtime errors.

-- โœ… Good: Type checking
if typeof(value) == "number" and value >= 0 and value <= 255 then
    local b = Serializer:serialize(nil, value)
end

โš ๏ธ Warnings

โš ๏ธ Schema Mismatches

Warning: If you modify your schema, old serialized buffers will be incompatible. Plan schema changes carefully or implement versioning.

-- Schema change breaks compatibility
-- Old: Types.Int32, Types.String
-- New: Types.Int32, Types.String, Types.Bool
-- Old buffers cannot be deserialized with new schema!

โš ๏ธ Compression Usage Recommendations

Warning: Compression is not always beneficial. Use compression carefully based on your data characteristics.

  • Not recommended for small data: Compression overhead may exceed benefits for buffers under 1KB. The compression algorithm itself adds overhead that can make small buffers larger.
  • Ineffective for unique data: Compression works best with repetitive patterns. If your data consists of mostly unique bytes (random or encrypted data), compression may actually increase the buffer size rather than reduce it.
  • CPU vs Bandwidth trade-off: Compression reduces bandwidth usage but increases CPU usage for both serialization and deserialization. Consider your game's performance profile before enabling compression.
  • Best for: Large structured data (>1KB) with repeated patterns, data sent infrequently, or when bandwidth is more constrained than CPU.
-- โŒ Not recommended: Small data
local smallData = Serializer:serialize(nil, 42, "hello")  -- ~10 bytes
-- Compression adds overhead, may increase size

-- โœ… Good: Large structured data
local largeData = Serializer:serialize(5, hugeTable, manyStrings)  -- 5KB+
-- Compression likely beneficial

-- โŒ Not recommended: Random/unique data
local encryptedData = Serializer:serialize(5, encryptedBuffer)
-- Random bytes don't compress well, may increase size

โš ๏ธ Offset Usage

Warning: If you use an offset during serialization, you must use the same offset during deserialization.

-- โœ… Correct: Same offset
local b = Serializer:serialize(10, data)
local result = Serializer:deserialize(10, b)

-- โŒ Wrong: Different offsets
local b = Serializer:serialize(10, data)
local result = Serializer:deserialize(0, b)  -- Will fail!

โš ๏ธ Type Limits

Warning: Be aware of type limits to avoid runtime errors.

  • StringTiny: Max 255 characters
  • ArrayTiny: Max 255 elements
  • String / Array: Max 65,535 characters/elements
  • Numeric types have fixed ranges

โš ๏ธ Enum Type Requirements

Warning: When using Types.Enum, you must provide the Enum type and ensure the value belongs to that Enum.

-- โœ… Correct
local MaterialEnum = Types.Enum(Enum.Material)
local b = Serializer:serialize(nil, Enum.Material.Plastic)

-- โŒ Wrong: Type mismatch
local MaterialEnum = Types.Enum(Enum.Material)
local b = Serializer:serialize(nil, Enum.HumanoidStateType.Running)  -- Error!

Credits & Update Logs

Credits

Author

IAMNOTULTRA3 (a.k.a elentium/elite)

License

This project is licensed under the Apache 2.0 License.

See the LICENSE file for details.

Repository

Package: elentium/voidsentry@0.0.7

Github: https://github.com/Elentium/VoidSentry

Support

For questions, issues, or feature requests, please open an issue on the repository or contact the author.

Changelog

Version 0.0.7

  • Added Instance type
  • Reworked serializers(instead of making 2 iterations for serialization, it uses only one, by dynamically extending buffer when there is not enough space)

Version 0.0.6

  • Fixed default.project.json

Version 0.0.5

  • Created GitHub repo & documentation
  • Published the package

Version 0.0.4

  • Changed some type names (e.g., Short โ†’ Int16)

Version 0.0.3

  • Fixed the way any type handled enums
  • Small optimizations
  • Better comments

Version 0.0.2

  • Improved README
  • Optimized Array, ArrayTiny, ArrayFixed types
  • Removed redundancy & inconsistencies

Version 0.0.1

  • Initial release

Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.