Torque

High-performance JSON library for Elixir via Rustler NIFs, powered by sonic-rs (SIMD-accelerated).

Torque provides among the fastest JSON encoding and decoding available in the BEAM ecosystem, with a selective field extraction API for workloads that only need a subset of fields from each document.

Features

Installation

Add to your mix.exs:

def deps do
[
{:torque, "~> 0.1.10"}
]
end

Precompiled binaries are available for common targets. To compile from source, install a stable Rust toolchain and set TORQUE_BUILD=true.

CPU-optimized variants

On x86_64, precompiled binaries are available for three CPU feature levels:

VariantCPU featurestarget-cpu
baselineSSE2x86-64
v2SSE4.2, SSSE3, POPCNTx86-64-v2
v3AVX2, AVX, BMI1, BMI2, FMAx86-64-v3

At compile time, Torque auto-detects the host CPU and downloads the best matching variant. To override detection (e.g., when cross-compiling for a different target):

TORQUE_CPU_VARIANT=v2 mix compile # force SSE4.2 variant
TORQUE_CPU_VARIANT=v3 mix compile # force AVX2 variant
TORQUE_CPU_VARIANT=base mix compile # force baseline

Usage

Decoding

{:ok, data} = Torque.decode(~s({"name":"Alice","age":30}))
# %{"name" => "Alice", "age" => 30}
data = Torque.decode!(json)

Selective Field Extraction

Parse once, extract many fields without building the full Elixir term tree:

{:ok, doc} = Torque.parse(json)
{:ok, "example.com"} = Torque.get(doc, "/site/domain")
nil = Torque.get(doc, "/missing/field", nil)
# Batch extraction (single NIF call, fastest path)
results = Torque.get_many(doc, ["/id", "/site/domain", "/device/ip"])
# [{:ok, "req-1"}, {:ok, "example.com"}, {:ok, "1.2.3.4"}]

When your JSON is known to have no duplicate object keys, pass unique_keys: true for faster field lookups (uses sonic-rs internal indexing instead of linear scan):

{:ok, doc} = Torque.parse(json, unique_keys: true)

Encoding

# Maps with atom or binary keys
{:ok, json} = Torque.encode(%{id: "abc", price: 1.5})
# "{\"id\":\"abc\",\"price\":1.5}"
# Bang variant
json = Torque.encode!(%{id: "abc"})
# iodata variant (fastest, no {:ok, ...} tuple wrapping)
json = Torque.encode_to_iodata(%{id: "abc"})
# jiffy-compatible proplist format
{:ok, json} = Torque.encode({[{:id, "abc"}, {:price, 1.5}]})

API

FunctionDescription
Torque.decode(binary)Decode JSON to Elixir terms
Torque.decode!(binary)Decode JSON, raising on error
Torque.parse(binary, opts)Parse JSON into opaque document reference
Torque.get(doc, path)Extract field by JSON Pointer path
Torque.get(doc, path, default)Extract field with default for missing paths
Torque.get_many(doc, paths)Extract multiple fields in one NIF call
Torque.get_many_nil(doc, paths)Extract multiple fields, nil for missing
Torque.length(doc, path)Return length of array at path
Torque.encode(term)Encode term to JSON binary
Torque.encode!(term)Encode term, raising on error
Torque.encode_to_iodata(term)Encode term, returns binary directly (fastest)

Type Conversion

JSON to Elixir

JSONElixir
objectmap (binary keys)
arraylist
stringbinary
integerinteger
floatfloat
true, falsetrue, false
nullnil

For objects with duplicate keys, the last value wins (unless unique_keys: true is passed to parse/2).

Elixir to JSON

ElixirJSON
map (atom/binary keys)object
listarray
binarystring
integernumber
floatnumber
true, falsetrue, false
nilnull
atomstring
{keyword_list}object

Errors

Functions return {:error, reason} tuples (or raise ArgumentError for bang/iodata variants). Possible reason atoms:

Decode / Parse

AtomReturned byMeaning
:nesting_too_deepdecode/1, get/2, get_many/2Document exceeds 128 nesting levels

parse/1 and decode/1 also return {:error, binary} with a message from sonic-rs for malformed JSON.

Encode

AtomReturned byMeaning
:unsupported_typeencode/1Term has no JSON representation (PID, reference, port, …)
:invalid_utf8encode/1Binary string or map key is not valid UTF-8
:invalid_keyencode/1Map key is not an atom or binary (e.g. integer key)
:malformed_proplistencode/1{proplist} contains a non-{key, value} element
:non_finite_floatencode/1Float is infinity or NaN (unreachable from normal BEAM code)
:nesting_too_deepencode/1Term exceeds 128 nesting levels

Benchmarks

Apple M2 Pro, OTP 29, Elixir 1.20:

Decode (1.2 KB OpenRTB)

Libraryipsmeanmedianp99memory
glazer371.2K2.69 μs2.50 μs6.42 μs1.56 KB
torque284.3K3.52 μs3.42 μs5.71 μs1.56 KB
jiffy215.7K4.64 μs4.21 μs8.71 μs1.55 KB
simdjsone194.9K5.13 μs4.96 μs8.96 μs1.59 KB
otp json145.6K6.87 μs6.58 μs11.63 μs7.73 KB
jason113.4K8.82 μs8.42 μs16.08 μs9.54 KB

Decode (750 KB Twitter)

Libraryipsmeanmedianp99memory
glazer592.91.69 ms1.50 ms2.21 ms1.58 KB
torque543.61.84 ms1.66 ms2.36 ms1.57 KB
simdjsone438.42.28 ms1.80 ms3.40 ms1.59 KB
jiffy311.53.21 ms3.31 ms3.71 ms2.30 MB
otp json213.14.69 ms4.73 ms5.68 ms2.48 MB
jason152.06.58 ms6.58 ms6.88 ms3.52 MB

Encode (1.2 KB OpenRTB)

Libraryipsmeanmedianp99memory
torque [proplist() :: iodata()]1260K0.79 μs0.71 μs0.92 μs64 B
torque [proplist() :: binary()]1250K0.80 μs0.75 μs0.92 μs88 B
otp json [map() :: iodata()]1180K0.85 μs0.79 μs1.25 μs3928 B
torque [map() :: binary()]1050K0.96 μs0.88 μs1.04 μs88 B
glazer [map() :: binary()]1020K0.98 μs0.83 μs1.63 μs64 B
torque [map() :: iodata()]1010K0.99 μs0.88 μs1.96 μs64 B
jiffy [proplist() :: iodata()]730K1.37 μs1.17 μs1.63 μs120 B
jason [map() :: iodata()]610K1.63 μs1.50 μs2.63 μs3848 B
jiffy [map() :: iodata()]590K1.71 μs1.42 μs3.71 μs824 B
simdjsone [proplist() :: iodata()]450K2.23 μs2.04 μs2.75 μs184 B
simdjsone [map() :: iodata()]370K2.67 μs2.42 μs4.67 μs888 B
jason [map() :: binary()]290K3.48 μs2.42 μs20.04 μs3912 B

Encode (750 KB Twitter)

Libraryipsmeanmedianp99memory
torque [proplist() :: binary()]1245.80.80 ms0.79 ms0.98 ms88 B
torque [proplist() :: iodata()]1241.50.81 ms0.79 ms0.98 ms64 B
torque [map() :: binary()]1121.00.89 ms0.88 ms1.05 ms88 B
torque [map() :: iodata()]1096.70.91 ms0.90 ms1.08 ms64 B
glazer [map() :: binary()]1001.01.00 ms0.99 ms1.10 ms64 B
jiffy [proplist() :: iodata()]544.71.84 ms1.83 ms2.04 ms37.7 KB
jiffy [map() :: iodata()]402.62.48 ms2.63 ms2.97 ms1.06 MB
otp json [map() :: iodata()]277.63.60 ms3.40 ms4.71 ms5.40 MB
simdjsone [proplist() :: iodata()]258.63.87 ms3.81 ms5.82 ms37.7 KB
jason [map() :: iodata()]240.04.17 ms3.91 ms6.22 ms4.96 MB
simdjsone [map() :: iodata()]218.64.57 ms4.66 ms5.31 ms1.06 MB
jason [map() :: binary()]129.67.72 ms7.71 ms8.34 ms4.96 MB

Parse (1.2 KB OpenRTB)

Libraryipsmeanmedianp99
torque parse(unique_keys)570.0K1.75 μs1.33 μs5.79 μs
torque parse545.2K1.83 μs1.33 μs6.08 μs
simdjsone parse360.1K2.78 μs1.17 μs5.63 μs

Get (5 fields) (1.2 KB OpenRTB)

Libraryipsmeanmedianp99memory
glazer find (decoded)2.83M353 ns333 ns459 ns424 B
torque get_many_nil (unique_keys)2.43M411 ns375 ns500 ns240 B
torque get_many (unique_keys)2.36M423 ns375 ns500 ns360 B
torque get_many_nil2.13M469 ns458 ns584 ns240 B
torque get_many2.04M491 ns458 ns625 ns360 B
simdjsone get1.76M568 ns458 ns1000 ns384 B
torque get (unique_keys)1.56M641 ns584 ns792 ns384 B
torque get1.41M709 ns667 ns916 ns384 B

glazer find runs over a fully decoded term (decode cost excluded, as parse cost is excluded for torque/simdjsone); glazer has no parse-to-handle API, so it is absent from the parse benchmark.

Run benchmarks locally:

MIX_ENV=bench mix run bench/torque_bench.exs

Limitations

License

MIT