Skip to content
C++
Domain Deep-Dive
Expert

FIX Protocol Parsing in C++

Low-latency FIX protocol parsing in C++: zero-allocation parsers, field lookup tables, FIXT 1.1 session management, and sub-microsecond message handling.

FIX protocol fundamentals

FIX (Financial Information eXchange) is the dominant electronic trading protocol. Messages look like:

cpp
8=FIX.4.4|9=149|35=D|49=CLIENTID|56=BROKER|34=1|52=20240101-12:00:00|
11=ORDER001|21=1|55=AAPL|54=1|60=20240101-12:00:00|38=100|40=2|44=150.00|10=123|

Each field is tag=value separated by SOH (ASCII 0x01). The parser must be extremely fast — at a market maker, you receive millions of messages per second.


Zero-allocation parser

cpp
#include <string_view>
#include <array>
#include <cstdint>

// FIX field: tag + value as string_view (pointer into raw buffer)
struct FixField {
    uint32_t tag;
    std::string_view value;
};

// Pre-allocated field array — no heap allocation during parse
struct FixMessage {
    static constexpr int MAX_FIELDS = 64;
    std::array<FixField, MAX_FIELDS> fields;
    int field_count = 0;

    // O(1) lookup via tag-indexed array (tags 1-999)
    // For sparse lookups a linear scan is often faster (cache-hot, branch-free)
    std::string_view Get(uint32_t tag) const {
        for (int i = 0; i < field_count; ++i)
            if (fields[i].tag == tag) return fields[i].value;
        return {};
    }
};

class FixParser {
public:
    // Parse in-place: all string_views point into buf
    // Returns number of bytes consumed (full message), or 0 if incomplete
    int Parse(const char* buf, int len, FixMessage& out) {
        out.field_count = 0;
        const char* p = buf;
        const char* end = buf + len;

        while (p < end) {
            // Find '='
            const char* eq = static_cast<const char*>(
                std::memchr(p, '=', end - p));
            if (!eq) return 0; // incomplete

            // Parse tag (no atoi — manual, faster)
            uint32_t tag = 0;
            for (const char* t = p; t < eq; ++t)
                tag = tag * 10 + (*t - '0');

            // Find SOH (0x01)
            const char* soh = static_cast<const char*>(
                std::memchr(eq + 1, '\x01', end - eq - 1));
            if (!soh) return 0; // incomplete

            if (out.field_count < FixMessage::MAX_FIELDS) {
                out.fields[out.field_count++] = {
                    tag,
                    std::string_view(eq + 1, soh - eq - 1)
                };
            }

            // Tag 10 = checksum = last field
            if (tag == 10) return static_cast<int>(soh - buf + 1);

            p = soh + 1;
        }
        return 0; // incomplete
    }
};

Fast tag-to-field lookup with a direct table

For the hottest path (checking a handful of known tags), a direct lookup table beats a loop:

cpp
// Tags of interest
enum FixTag : uint32_t {
    BeginString   = 8,
    MsgType       = 35,
    Symbol        = 55,
    Side          = 54,
    OrderQty      = 38,
    Price         = 44,
    OrdType       = 40,
    ClOrdID       = 11,
    ExecType      = 150,
    OrdStatus     = 39,
    LastPx        = 31,
    LastQty       = 32,
    LeavesQty     = 151,
    CumQty        = 14,
};

// Index array: slot[tag] = index into FixMessage::fields (-1 if absent)
// Tag range 1-200 covers 95% of FIX fields
struct FixIndex {
    static constexpr int TAG_RANGE = 201;
    std::array<int8_t, TAG_RANGE> slot;

    void Build(const FixMessage& msg) {
        slot.fill(-1);
        for (int i = 0; i < msg.field_count; ++i) {
            if (msg.fields[i].tag < TAG_RANGE)
                slot[msg.fields[i].tag] = static_cast<int8_t>(i);
        }
    }

    std::string_view Get(const FixMessage& msg, uint32_t tag) const {
        if (tag >= TAG_RANGE || slot[tag] < 0) return {};
        return msg.fields[slot[tag]].value;
    }
};

String-to-number conversion (no strtod overhead)

cpp
// Parse integer field — much faster than atoi/strtol
inline int64_t ParseInt(std::string_view sv) noexcept {
    int64_t result = 0;
    bool neg = (!sv.empty() && sv[0] == '-');
    for (char c : sv.substr(neg ? 1 : 0))
        result = result * 10 + (c - '0');
    return neg ? -result : result;
}

// Parse fixed-point price (e.g., "123.45" → 12345 in 1/100 units)
inline int64_t ParsePrice(std::string_view sv, int decimals = 2) noexcept {
    int64_t integer = 0, frac = 0;
    int frac_digits = 0;
    bool in_frac = false;

    for (char c : sv) {
        if (c == '.') { in_frac = true; continue; }
        if (!in_frac) {
            integer = integer * 10 + (c - '0');
        } else if (frac_digits < decimals) {
            frac = frac * 10 + (c - '0');
            ++frac_digits;
        }
    }

    while (frac_digits++ < decimals) frac *= 10;
    return integer * static_cast<int64_t>(std::pow(10, decimals)) + frac;
}

FIXT 1.1 session management

The FIX session layer handles logon, heartbeats, sequence numbers, and resend requests:

cpp
class FixSession {
    int m_inbound_seq  = 1;
    int m_outbound_seq = 1;
    bool m_logged_on   = false;

public:
    void OnMessage(const FixMessage& msg) {
        auto msg_type = msg.Get(FixTag::MsgType);

        // Sequence number check
        int msg_seq = static_cast<int>(ParseInt(msg.Get(34)));
        if (msg_seq != m_inbound_seq) {
            SendResendRequest(m_inbound_seq, msg_seq - 1);
            return;
        }
        ++m_inbound_seq;

        if (msg_type == "A") { // Logon
            m_logged_on = true;
            SendLogon();
        } else if (msg_type == "0") { // Heartbeat
            // Respond if TestReqID present
            if (!msg.Get(112).empty()) SendHeartbeat(msg.Get(112));
        } else if (msg_type == "1") { // Test Request
            SendHeartbeat(msg.Get(112));
        } else if (msg_type == "2") { // Resend Request
            HandleResend(msg);
        } else if (m_logged_on) {
            DispatchApplicationMessage(msg, msg_type);
        }
    }

    void SendNewOrder(std::string_view symbol, char side,
                      int64_t qty, int64_t price_cents) {
        // Build FIX message into stack buffer — no heap
        char buf[512];
        int n = snprintf(buf, sizeof(buf),
            "35=D\x01" "11=ORD%d\x01" "55=%.*s\x01"
            "54=%c\x01" "38=%lld\x01" "44=%lld.%02lld\x01" "40=2\x01",
            m_outbound_seq++,
            (int)symbol.size(), symbol.data(),
            side, qty / 100, qty % 100,
            price_cents / 100, price_cents % 100);
        SendRaw(buf, n);
    }
};

Benchmark: parser throughput

A well-optimized FIX parser on modern hardware:

Message sizeThroughputLatency
150 bytes~50M msg/s~20 ns
300 bytes~25M msg/s~40 ns
600 bytes~12M msg/s~80 ns

Key optimizations that get you there:

  • memchr is SIMD-accelerated by glibc — use it for SOH search
  • Manual tag parsing (no atoi) saves ~5 ns per field
  • Parse directly into pre-allocated struct — no std::string
  • __builtin_expect on the happy path
  • Avoid branch mispredictions: message types are predictable (most are ExecutionReports)
Edit on GitHubUpdated 2026-05-24T00:00:00.000Z