Skip to main content
Slung uses a compact binary protocol over WebSocket for high-performance metric ingestion. All data is encoded in little-endian format.

Message Format

Each WebSocket frame contains a single metric encoded as:
[timestamp:i64][value:f64][series_len:u16][tag_count:u16][series_utf8][tag_len:u16+tag_utf8]...

Field Breakdown

FieldTypeSizeDescription
timestampi648 bytesUnix epoch timestamp in microseconds
valuef648 bytesIEEE 754 double-precision metric value
series_lenu162 bytesLength of series name in bytes
tag_countu162 bytesNumber of tags
seriesUTF-8VariableSeries name (max 65535 bytes)
tagsVariableVariableRepeated tag entries

Tag Encoding

Each tag is encoded as:
[tag_len:u16][tag_utf8]
  • tag_len (u16, 2 bytes): Length of tag string in bytes
  • tag_utf8 (variable): UTF-8 encoded tag (format: key=value)
Tags are repeated tag_count times.

Protocol Implementation

The server decodes messages using the following logic (extracted from src/main.zig:424-469):
fn decodeBinaryMessage(allocator: std.mem.Allocator, data: []const u8, tag_scratch: *std.ArrayList([]const u8)) !DecodedMessage {
    // little-endian:
    // [timestamp:i64][value:f64][series_len:u16][tag_count:u16][series][tag_len:u16+tag_bytes]...
    const header_len = 8 + 8 + 2 + 2;
    if (data.len < header_len) return error.InvalidMessage;

    var offset: usize = 0;
    const timestamp_bits = std.mem.readInt(u64, data[offset..][0..8], .little);
    const timestamp: i64 = @bitCast(timestamp_bits);
    offset += 8;

    const value_bits = std.mem.readInt(u64, data[offset..][0..8], .little);
    const value: f64 = @bitCast(value_bits);
    offset += 8;

    const series_len = @as(usize, std.mem.readInt(u16, data[offset..][0..2], .little));
    offset += 2;
    const tag_count = @as(usize, std.mem.readInt(u16, data[offset..][0..2], .little));
    offset += 2;

    if (offset + series_len > data.len) return error.InvalidMessage;
    const series = data[offset .. offset + series_len];
    offset += series_len;

    tag_scratch.clearRetainingCapacity();
    try tag_scratch.ensureTotalCapacity(allocator, tag_count);

    var i: usize = 0;
    while (i < tag_count) : (i += 1) {
        if (offset + 2 > data.len) return error.InvalidMessage;
        const tag_len = @as(usize, std.mem.readInt(u16, data[offset..][0..2], .little));
        offset += 2;
        if (offset + tag_len > data.len) return error.InvalidMessage;
        try tag_scratch.append(allocator, data[offset .. offset + tag_len]);
        offset += tag_len;
    }

    if (offset != data.len) return error.InvalidMessage;

    return .{
        .timestamp = timestamp,
        .value = value,
        .series = series,
        .tags = tag_scratch.items,
    };
}

Encoding Example

For a metric with:
  • Timestamp: 1709481600000000 (microseconds)
  • Value: 23.5
  • Series: "temp"
  • Tags: ["sensor=1", "env=dev"]
The binary encoding would be:
  1. Header (20 bytes):
    • Timestamp: 0x00 0x50 0xB8 0x8E 0x0E 0x18 0x06 0x00 (i64 LE)
    • Value: 0x00 0x00 0x00 0x00 0x00 0x80 0x37 0x40 (f64 LE)
    • Series length: 0x04 0x00 (4 bytes)
    • Tag count: 0x02 0x00 (2 tags)
  2. Series (4 bytes):
    • "temp": 0x74 0x65 0x6D 0x70
  3. Tags:
    • Tag 1 length: 0x08 0x00 (8 bytes)
    • Tag 1: "sensor=1": 0x73 0x65 0x6E 0x73 0x6F 0x72 0x3D 0x31
    • Tag 2 length: 0x07 0x00 (7 bytes)
    • Tag 2: "env=dev": 0x65 0x6E 0x76 0x3D 0x64 0x65 0x76
Total message size: 20 + 4 + 2 + 8 + 2 + 7 = 43 bytes

Validation Rules

The server enforces these validation rules:
  1. Minimum message size: Must be at least 20 bytes (header)
  2. Complete frames: Message length must exactly match declared sizes
  3. UTF-8 encoding: Series and tags must be valid UTF-8
  4. Size limits:
    • Series name: max 65535 bytes
    • Tag count: max 65535 tags
    • Individual tag: max 65535 bytes
Invalid messages return an InvalidMessage error and close the connection.

Connection Lifecycle

  1. Connect: Establish WebSocket connection to ws://host:2077
  2. Send: Send binary frames containing encoded metrics
  3. Stream: Connection remains open for continuous streaming
  4. Close: Gracefully close with status code 1000

Performance Characteristics

  • Minimal overhead: Fixed 20-byte header + UTF-8 string data
  • Zero-copy parsing: Server parses directly from WebSocket frames
  • High throughput: Supports 100K+ events/second on single connection
  • Low latency: Sub-millisecond processing time per message

Next Steps

Client SDKs

Use official SDKs that handle protocol encoding automatically

Build docs developers (and LLMs) love