Codecs

Last edited 5 minutes ago.

On this page

Every task that flows through an Apalis backend is serialized before storage and deserialized before your handler receives it. This conversion is handled by a codec — a type that implements the Codec trait. A backend exposes its codec through the WireFormatBackend trait, and you can replace it with the with_codec combinator on BackendExt.

apalis-codec provides ready-made codecs for the most common formats, and the Codec trait is straightforward to implement for custom needs.


The Codec Trait

pub trait Codec<T> {
    /// The encoded (stored) representation of `T`.
    type Compact;
    /// The error type returned if encoding or decoding fails.
    type Error;

    /// Encode a value into its compact representation.
    fn encode(input: &T) -> Result<Self::Compact, Self::Error>;

    /// Decode a compact representation back into `T`.
    fn decode(compact: &Self::Compact) -> Result<T, Self::Error>;
}

The Compact associated type is the backend's storage format — for example Vec<u8> for binary formats or String for text-based ones. T is your job argument type. The codec sits between the two, encoding on the way in and decoding on the way out.

You generally never call Codec methods directly — TaskSink calls encode when pushing, and the backend calls decode before yielding a task from poll_next to the worker.


Available Codecs

apalis-codec ships three codecs behind feature flags:

FeatureCodecCompact typeFormat
json (default)JsonCodecVec<u8>JSON bytes via serde_json
msgpackMsgPackCodecVec<u8>MessagePack via rmp-serde
bincodeBincodeCodecVec<u8>Bincode via the bincode crate

All three require your job type to implement serde::Serialize and serde::Deserialize.

JSON (default)

apalis-codec = "1"

JSON is the default codec used by most Apalis backends. It produces human-readable payloads, which makes debugging straightforward — you can inspect stored tasks directly in your database.

use apalis_codec::json::JsonCodec;

let encoded: Vec<u8> = JsonCodec::encode(&my_job)?;
let decoded: MyJob  = JsonCodec::decode(&encoded)?;

MessagePack

apalis-codec = { version = "1", features = ["msgpack"] }

MessagePack produces compact binary payloads — typically 20–50% smaller than equivalent JSON. Use it when storage size or serialization throughput matters and human-readable payloads are not required.

use apalis_codec::msgpack::MsgPackCodec;

let encoded: Vec<u8> = MsgPackCodec::encode(&my_job)?;
let decoded: MyJob  = MsgPackCodec::decode(&encoded)?;

Bincode

apalis-codec = { version = "0.1", features = ["bincode"] }

Bincode is a compact, non-self-describing binary format optimised for speed. It produces the smallest payloads of the three options but is the least portable — decoded data must be read with the same type definition it was encoded with.

use apalis_codec::bincode::BincodeCodec;

let encoded: Vec<u8> = BincodeCodec::encode(&my_job)?;
let decoded: MyJob  = BincodeCodec::decode(&encoded)?;

Choosing a Codec

ConsiderationJSONMessagePackBincode
Human-readable payloads✓✗✗
Payload sizeLargestSmallerSmallest
Serialization speedModerateFastFastest
Schema flexibilityHighHighLow
Debugging easeEasyModerateDifficult
Cross-language compatibility✓✓✗

JSON is the right default for most applications — readable, flexible, and widely supported. Switch to MessagePack when you have high task volume and want to reduce storage overhead without sacrificing schema flexibility. Use Bincode only in performance-critical, Rust-only pipelines where you control both the producer and consumer.


How Codecs Integrate with Backends

A backend declares its codec through the WireFormatBackend trait, which names the codec, the compact storage type it produces, and gives access to the codec instance:

pub trait WireFormatBackend {
    type Codec;
    type Compact;

    fn codec(&self) -> &Self::Codec;
}

Every backend picks a default codec, and all tasks pushed to that backend use it. When you call TaskSink::push, the codec's encode is called automatically. When the worker polls and receives a task, decode is called before the handler sees the arguments.

If encoding fails, a TaskSinkError::CodecError is returned. If decoding fails, the task is surfaced as an error on the worker's stream.

Swapping a Backend's Codec

The codec is no longer fixed to a backend type. BackendExt provides a with_codec combinator that swaps out the serialization codec entirely — JSON, MessagePack, Protobuf, or your own — without touching storage logic:

pub trait BackendExt: Backend {
    /// Swaps out the backend's serialization codec entirely (JSON,
    /// MessagePack, Protobuf, ...) without touching storage logic.
    fn with_codec<NewCodec>(self, codec: NewCodec) -> WithCodec<Self, NewCodec>
    where
        Self: Sized + BackendConfig,
        NewCodec: Codec<Self::Args>,
    {
        WithCodec::new(self, codec)
    }
}

with_codec consumes the backend and returns a WithCodec wrapper. The wrapper implements WireFormatBackend, so downstream code sees the new codec and its compact type:

impl<B, NewCodec, Args> WireFormatBackend for WithCodec<B, NewCodec>
where
    NewCodec: Codec<Args> + Send + 'static,
    B: BackendConfig<Args = Args>,
{
    type Codec = NewCodec;
    type Compact = NewCodec::Compact;

    fn codec(&self) -> &Self::Codec {
        &self.codec
    }
}

Two bounds matter here: the codec must be able to encode and decode the backend's Args type, and it must be Send + 'static so it can live inside a running worker.

Using it looks like this:

use apalis_codec::msgpack::MsgPackCodec;

let backend = PostgresStorage::new(&pool).with_codec(MsgPackCodec);

let worker = WorkerBuilder::new("tasty-banana")
    .backend(backend)
    .build(task_fn);

Because the new codec determines the compact type, make sure the backend's storage can hold it. A backend that stores payloads in a binary column works with a Vec<u8> codec such as MessagePack, but not necessarily with one that produces String. Check the backend's documentation for the compact types it supports.


Implementing a Custom Codec

If the built-in codecs do not suit your needs — for example, you need to store tasks as serde_json::Value for schema-flexible querying, or you have a proprietary binary format — implement Codec directly:

use apalis_core::backend::codec::Codec;

pub struct MyCodec;

impl<T> Codec<T> for MyCodec
where
    T: serde::Serialize + for<'de> serde::Deserialize<'de>,
{
    type Compact = String;
    type Error   = serde_json::Error;

    fn encode(input: &T) -> Result<String, Self::Error> {
        serde_json::to_string(input)
    }

    fn decode(compact: &String) -> Result<T, Self::Error> {
        serde_json::from_str(compact)
    }
}

Here Compact = String rather than Vec<u8> — useful for backends like PostgreSQL where storing tasks as a TEXT or JSONB column makes them directly queryable with SQL.

Then attach it to a backend with with_codec:

let backend = PostgresStorage::new(&pool).with_codec(MyCodec);

Summary

CodecFeature flagBest for
JsonCodecjson (default)Debuggability, cross-language compatibility
MsgPackCodecmsgpackReduced payload size, high throughput
BincodeCodecbincodeMaximum performance in Rust-only pipelines
Custom—Proprietary formats, queryable storage types
TraitRole
Codec<T>Encodes and decodes T to and from a compact representation
WireFormatBackendExposes a backend's codec and compact type
BackendExt::with_codecSwaps a backend's codec without changing its storage logic

Codecs are the only layer in Apalis that touches the raw bytes of your task payload. Keeping them swappable — through a trait and a with_codec combinator, rather than a hardcoded format — means you can change serialization strategy per backend without touching your handler logic.