On this page

cppvalley engineering note

How a C++ HFT System Reduced Actor Messaging From 3370ns to 30ns

A student-friendly explanation of fast_send, actor groups, thread handoffs, memory pools and why crossing a thread can dominate low-latency C++ messaging costs.

Understanding fast_send, Actor Systems, Thread Handoffs, Memory Pools, and Low-Latency C++

High-frequency trading systems care about time at a scale most programmers rarely think about.

A normal web application might consider a response time of 50 milliseconds fast.

An HFT system may care about differences of a few microseconds or even nanoseconds.

For reference:

1 second      = 1,000 milliseconds
1 millisecond = 1,000 microseconds
1 microsecond = 1,000 nanoseconds

So:

1 millisecond = 1,000,000 nanoseconds

This means that when an HFT engineer worries about 30 nanoseconds, they are optimizing something that takes only:

0.000000030 seconds

A September 2026 research paper by Vincent Maciejewski investigates an interesting question:

Can we use a clean concurrency model called the Actor Model in an HFT system without making the system too slow?

The paper presents an open-source C++20 HFT system called kaspar-hft and introduces an optimization called fast_send.

The most eye-catching benchmark from the paper looks like this:

Normal actor communication:     ~3370 ns
Actors sharing one thread:         90 ns
fast_send:                         ~30 ns

That is roughly a 110× difference between the normal cross-thread actor implementation and fast_send in this particular microbenchmark.

But the interesting part is not the number.

The interesting part is understanding where those missing nanoseconds went.


1. First: What Is High-Frequency Trading?

At a very simplified level, an electronic trading system may receive information like:

Apple:
Bid = $200.10
Ask = $200.11

A trading system might perform something like:

Exchange
   ↓
Network packet
   ↓
Decode market data
   ↓
Update order book
   ↓
Run trading strategy
   ↓
Risk checks
   ↓
Send order
   ↓
Exchange

Every box takes time.

In some trading systems, even a few extra microseconds can matter.

This is why HFT engineers care deeply about:

CPU caches
memory allocation
threads
context switches
networking
branch prediction
data structures
locks
atomics
NUMA
operating-system scheduling

The research paper focuses mainly on the software architecture and concurrency part of this problem.


2. The Programming Problem

Imagine we are building a small trading system.

We could create components like:

MarketDataDecoder
OrderBook
Strategy
RiskManager
OrderGateway

One simple design would allow every component to directly modify shared data.

For example:

class OrderBook {
public:
    double best_bid;
    double best_ask;
};

Then multiple threads could access:

book.best_bid
book.best_ask

But now we have a problem.

Suppose Thread A is updating the book while Thread B is reading it.

We might get:

Thread A:
best_bid = 100

Thread B:
reads best_bid

Thread A:
best_ask = 101

Thread B may observe the object halfway through an update.

This is a simplified example of the larger problem of shared mutable state.

Normally we solve these problems using tools such as:

std::mutex
std::atomic
std::shared_mutex

But concurrent code can become difficult very quickly.

You have to think about questions like:

Who owns this object?

Which mutex protects it?

Can these two locks deadlock?

What happens if another thread modifies the object here?

Which memory ordering should this atomic operation use?

The Actor Model offers another approach.


3. What Is the Actor Model?

Think of an actor as a small worker with its own private room.

Each worker has:

private data
+
an inbox
+
the ability to process messages

Nobody is allowed to walk into another worker's room and change their data.

Instead, you send them a message.

For example:

Strategy Actor
      |
      | "What is the current price?"
      ↓
Order Book Actor

The Order Book Actor reads the message and replies.

In code, the idea may look conceptually like this:

class OrderBookActor {
private:
    double best_bid_;
    double best_ask_;

public:
    Quote on(GetQuote message) {
        return {
            best_bid_,
            best_ask_
        };
    }
};

Notice something important.

These variables are private:

best_bid_
best_ask_

The strategy does not directly change them.

It communicates through messages.

This gives us a useful rule:

One actor owns its state.

That makes concurrent programs easier to reason about because actors process their own state sequentially. The paper argues that state isolation, sequential message processing, and avoiding manually shared mutable state are major advantages of this architecture.


4. Why Aren't Actors Used Everywhere in HFT?

Because traditional actor communication can be expensive.

Imagine Actor A wants Actor B to do something.

A traditional actor implementation might work like this:

Actor A
   ↓
Create message
   ↓
Put message in Actor B's queue
   ↓
Notify scheduler
   ↓
Actor B's thread wakes up
   ↓
Remove message from queue
   ↓
Run handler

If Actor B needs to reply:

Actor B
   ↓
Create reply
   ↓
Put reply in Actor A's queue
   ↓
Wake Actor A
   ↓
Actor A receives reply

That is a lot of machinery for something that may logically be equivalent to:

result = object.doSomething();

The paper identifies several important costs in the traditional path: message allocation, queue operations, scheduling, and thread/context switching.


5. What Is a Thread Handoff?

Suppose we have two CPU cores:

CPU Core 1               CPU Core 2

Actor A                   Actor B

Actor A produces some work.

Actor B needs to process it.

The data may have to move through shared cache/coherency structures, a queue must be synchronized, and the operating system or runtime may need to wake or schedule another thread.

Conceptually:

Core 1
  |
  | write message
  ↓
Shared memory / queue
  |
  | synchronization
  ↓
Core 2

Programmers sometimes see:

queue.push(message);

and mentally think:

"one function call"

The processor sees something more complicated.

Potential costs include:

locking
atomic instructions
cache-line movement
queue synchronization
thread wakeups
scheduler activity
lost cache locality

This is one reason why simply adding more threads does not automatically make software faster.

Sometimes another thread increases throughput.

Sometimes it just creates another expensive boundary.


6. The Main Idea of the Paper: fast_send

The paper asks:

What if Actor A and Actor B are inside the same process?

Do we really need this?

Actor A
   ↓
queue
   ↓
scheduler
   ↓
Actor B

Maybe not.

Instead, the paper introduces something called:

fast_send()

The idea is roughly:

Actor A's thread
      ↓
gain exclusive access to Actor B
      ↓
execute Actor B's handler immediately
      ↓
return result

So instead of:

A → Queue → Thread B → B

we get:

A → B

on the same executing thread.

The sender temporarily executes the receiver's message handler.

That eliminates the normal mailbox and thread-handoff path for this interaction.


7. A Simplified C++ Example

The real framework is more sophisticated, but we can understand the idea with a tiny example.

Suppose we have:

struct GetQuote {};

struct Quote {
    double bid;
    double ask;
};

And an actor:

class BookActor {
private:
    double best_bid_ = 100.00;
    double best_ask_ = 100.01;

public:
    Quote on(const GetQuote&) {
        return {
            best_bid_,
            best_ask_
        };
    }
};

Normally we might send a message asynchronously.

Conceptually:

send(book, GetQuote{});

That could mean:

put GetQuote into mailbox
wake book thread
book processes message later
send reply

With synchronous execution we want something closer to:

Quote quote = fast_send(book, GetQuote{});

A highly simplified teaching implementation might look like:

template <typename Actor, typename Message>
auto fast_send(Actor& actor, const Message& message)
{
    std::scoped_lock lock(actor.mutex);

    return actor.on(message);
}

Again, this is not the complete implementation from the research code.

It is just enough to understand the main idea.

The important part is:

return actor.on(message);

Actor B's handler runs immediately on Actor A's thread.


8. But Why Not Just Call a Normal Function?

At this point you may ask:

Why not simply write:

book.getQuote();

Excellent question.

The research is trying to preserve the actor abstraction.

The book is still supposed to be an independently owned unit of state.

Only one execution path should be allowed to modify it at a time.

fast_send therefore obtains exclusive access to the actor before executing its handler.

This gives us something conceptually between:

ordinary function call

and:

normal asynchronous actor message

We get function-call-like execution while keeping actor-style ownership.

The paper calls an important part of this design receiver transparency.

The receiving actor's handler should not care whether it was reached through normal asynchronous messaging or fast_send.

Think about this function:

Quote on(const GetQuote&) {
    return {
        best_bid_,
        best_ask_
    };
}

It doesn't contain:

if (fast_mode) {
    ...
}

or:

if (async_mode) {
    ...
}

The receiver simply handles the message.

The sender chooses the delivery mechanism.

This is good abstraction.


9. Unfortunately, fast_send Creates a New Problem

Suppose:

Actor A calls Actor B

A → B

Then B calls C:

A → B → C

No problem.

But now imagine C calls A:

A → B → C → A

We have a cycle.

Remember that A is already executing.

Its lock may already be held.

Now C tries to acquire A's lock.

But the current execution chain is already using A.

The result can be:

DEADLOCK

The thread waits for a lock that cannot become available because the same chain is already inside the actor.

With certain reentrant designs, another bad possibility would be endless recursive execution until the stack overflows.

The paper therefore needs a way to detect:

A → B → C → A

before acquiring A again.


10. Enter thread_local

The paper maintains information about the current synchronous actor call chain.

Conceptually:

thread_local std::vector<Actor*> call_chain;

Suppose execution currently looks like:

A → B → C

Then we store something like:

call_chain = [A, B, C]

Before C calls A, we check:

if (A is already inside call_chain)

If yes:

STOP

because a cycle exists.

A simplified teaching implementation could look like this:

thread_local std::vector<Actor*> call_chain;

template <typename ActorType, typename Message>
auto fast_send(
    ActorType& actor,
    const Message& message)
{
    Actor* target = &actor;

    auto found =
        std::find(
            call_chain.begin(),
            call_chain.end(),
            target
        );

    if (found != call_chain.end()) {
        throw std::runtime_error(
            "Cyclic actor call detected"
        );
    }

    std::unique_lock lock(actor.mutex);

    call_chain.push_back(target);

    auto result = actor.on(message);

    call_chain.pop_back();

    return result;
}

The important order is:

1. Check for cycle
2. Acquire actor lock
3. Record actor in current chain
4. Execute actor
5. Remove actor from chain
6. Release lock

The cycle check must happen before acquiring the dangerous lock. The paper's design uses thread-local call-chain tracking so this check itself does not require synchronization between threads.


11. fast_send Is Actually a Family of Operations

Not every situation should behave the same way.

What happens if another thread is currently using the actor?

The paper provides several possibilities.

A simplified view is:

OperationBehaviourMeaning
sendPut message in queue and continue
fast_sendWait until actor becomes available
fast_send_spinBusy-wait for actor
fast_send_xTry once and fail if actor is busy
fast_send_voidTry synchronously, otherwise fall back to async

Why so many?

Because low-latency engineering is about trade-offs.

Sometimes waiting is acceptable.

Sometimes waiting is unacceptable.

Sometimes wasting one CPU core spinning is acceptable because latency matters more than power consumption.

Sometimes you would rather fall back to a queue.

There is no universal answer.


12. Actor Groups

The paper introduces another important idea: Actor groups.

Imagine three actors:

Decoder
OrderBook
Strategy

A naive design could place them on three different threads:

Core 1          Core 2          Core 3

Decoder  →      OrderBook  →    Strategy

Every arrow may involve communication between threads.

But ask yourself:

Do these operations actually need to happen simultaneously?

Often the logic is naturally:

receive update
then decode it
then update book
then calculate strategy

They form a pipeline where each step depends on the previous one.

So the paper allows actors to share one execution thread:

              CPU Core

Decoder → OrderBook → Strategy

These actors form an actor group.

They can share one mailbox and execute on one core.

This reduces:

cross-core communication
scheduler activity
thread wakeups
cache movement

And then fast_send can remove even the internal queue operation between actors when synchronous execution makes sense.


13. The Big Benchmark

Now we can understand the paper's famous benchmark.

The authors measured a ping-pong round trip between actors.

Think:

Actor A
   ↓ ping
Actor B
   ↓ pong
Actor A

Version 1: Normal actors on different threads

Thread A
   ↓
queue
   ↓
Thread B
   ↓
queue
   ↓
Thread A

Measured round-trip latency:

~3370 ns

Version 2: Actors grouped on one thread

Now we remove the cross-thread handoff:

one thread

A → queue → B → queue → A

Result:

90 ns

Version 3: fast_send

Now remove the queue as well:

one thread

A → B → A

Result:

~30 ns

The paper summarizes the experiment approximately as:

Normal asynchronous send      ~3370 ns
Grouped asynchronous send        90 ns
fast_send                        ~30 ns

A single fast_send hop was around 10 ns in that experiment, while the authors' corresponding bare direct-call measurement was around 1 ns.

The most important conclusion is not:

fast_send is 110× faster!

Benchmarks depend on hardware, workload, compiler settings and many other things.

The more general lesson is:

The thread boundary was dramatically more expensive than the message handler itself.

14. This Changes How We Think About Multithreading

Many programmers learn:

more work
   ↓
more threads
   ↓
more performance

Reality is closer to:

Independent CPU-heavy work?
        ↓
Threads may help greatly.

But:

Tiny sequential dependent operations?
        ↓
Moving each step to another thread
may cost more than the work itself.

Imagine your actual calculation takes:

20 ns

But transferring the work to another thread costs:

1500 ns

You just spent far more time moving the work than doing the work.

This idea matters far beyond HFT.

It applies to:

game engines
databases
web servers
network servers
storage engines
real-time systems
AI inference runtimes

15. Memory Allocation Is Another Problem

Suppose asynchronous communication requires creating a message:

auto* message =
    new MarketUpdate(...);

The receiver may process it later.

Therefore the message must remain alive after the sender returns.

That often means using heap storage.

But new and delete are general-purpose operations.

General-purpose allocators must handle many object sizes, many threads, fragmentation, free lists, metadata and synchronization.

Most allocations may be reasonably fast.

But sometimes the allocator hits a slow path.

HFT does not only care about average latency. It cares about worst unusual latency.

The paper therefore uses a fixed-size memory pool for messages that must survive beyond the current call. A synchronous fast_send message can often remain on the sender's stack because the call does not return until processing finishes.


16. What Is a Memory Pool?

Suppose you know you frequently need objects of the same type.

Instead of repeatedly asking the general system allocator:

new Message;
delete message;

you reserve memory ahead of time:

[slot][slot][slot][slot][slot][slot]

When you need a message, you take a free slot. When finished, you return the slot.

This makes allocation behaviour more predictable.

In one experiment reported by the paper, enabling the pool reduced a maximum round-trip latency from about:

5.5 milliseconds

to:

33 microseconds

for the measured scenario.

Notice something interesting.

The memory pool is important not only because it improves average speed.

It improves predictability.

And predictability is extremely important in real-time and low-latency systems.


17. Welcome to Tail Latency

Suppose we measure a program five times:

10 μs
9 μs
10 μs
11 μs
10 μs

Looks great.

But imagine that once every thousand operations we get:

5000 μs

The average may still look reasonable.

But for a trading system, that one slow event could matter enormously.

This is why low-latency engineers look at percentiles.

For example:

p50
p90
p99
p99.9
p99.99

p50 means the median.

Roughly half of requests were faster and half were slower.

p99 means approximately 99% completed faster than this value.

The remaining 1% were slower.

An HFT system may have:

p50 = 7 μs

but:

p99 = 50 μs

Those numbers describe very different performance characteristics.

The live CME measurements in the paper show exactly this problem: median socket-to-book latencies could look similar while high-percentile tail latency differed substantially across streams.


18. The Real HFT Pipeline

The paper does not only run synthetic benchmarks.

The framework was also measured using live CME futures market data.

The simplified architecture looks something like:

CME Network
    ↓
Socket Reader
    ↓
Buffer
    ↓
Decoder
    ↓
Order Book
    ↓
Strategy
    ↓
Order Handler
    ↓
Exchange

The important architectural idea is that the system does not try to remove all asynchronous communication.

Instead, it places asynchronous boundaries where they make sense.

For example, the network-reading component should keep reading packets.

If it waits for the entire trading calculation after every packet, incoming packets could accumulate.

So:

Network Reader
      ↓
 asynchronous boundary
      ↓
Processing

makes sense.

Inside the processing stage:

Decoder
   ↓
Order Book
   ↓
Strategy

the steps are closely connected.

Putting a thread boundary between every one may simply add latency.

The reference architecture therefore keeps the main servicing chain together and uses synchronous communication there, while retaining asynchronous boundaries where independent progress is useful.

This leads to an important lesson:

Asynchronous programming is not bad. Synchronous programming is not bad. The correct choice depends on where the boundary is useful.

19. What Is Tick-to-Book Latency?

The paper measures something called socket-to-book latency, or approximately tick-to-book latency.

Simplified:

Market packet becomes available
        ↓
decode packet
        ↓
understand market event
        ↓
update internal order book

The measured baseline was approximately:

7 microseconds

for the first message in a packet under the paper's measured conditions.

Remember:

7 μs = 7000 ns

Compare that with:

fast_send ≈ 10 ns per hop

Even three such calls would be roughly:

3 × 10 ns = 30 ns

Compare:

30 ns
versus
7000 ns

That is why the paper concludes that the actor framework itself contributes less than 1% of the measured decode-and-book baseline.


20. An Unexpected Finding: Position Inside the Packet Matters

Suppose one network packet contains:

Message 0
Message 1
Message 2
Message 3
...
Message 40

The decoder processes messages sequentially.

Message 0 gets processed immediately.

But Message 40 has to wait for earlier messages in the packet.

The paper models the median latency approximately as:

latency = floor + slope × message_position

The measured floor was roughly 6.8–7.2 μs across the primary book streams studied, while the per-message slope varied substantially by stream. For the book streams shown in the paper, the reported slope ranged from about 312 ns/message to 966 ns/message.

Let's use a simple made-up example close to those magnitudes.

Suppose:

floor = 7 μs
slope = 0.5 μs
message position = 20

Then:

latency = 7 + (0.5 × 20) = 17 μs

Nothing necessarily became slower.

The message simply had to wait behind earlier messages in the packet.


21. Market Traffic Arrives in Bursts

Another common beginner assumption is that network traffic arrives smoothly.

Real markets can be bursty.

Imagine five packets arrive very quickly.

The system may currently be processing Packet 1.

The queue becomes:

Packet 2
Packet 3
Packet 4
Packet 5

Packet 5 has to wait.

Its latency increases even if your program has not changed.

The paper finds that queue occupancy and the position of a message inside its packet help explain important parts of the observed latency tail.

This gives us a deeper lesson:

Performance depends not only on how fast your code is, but also on how work arrives.

This matters in trading systems, web servers, databases, storage systems, distributed systems and AI inference servers.


22. Why Average Throughput Can Be Misleading

Imagine your system handles:

1,000,000 events per second

on average.

That sounds excellent.

But perhaps traffic arrives like:

first 900 ms: almost nothing
last 100 ms: huge burst

The average is still 1,000,000 per second, but your queue may overflow during the burst.

This is why low-latency systems engineers care about both throughput and latency under bursts.

A program can have impressive average throughput while still experiencing terrible tail latency.


23. The Paper Also Tests Different Queue Designs

The framework does not assume one magical queue is best.

It includes multiple mailbox implementations, including mutex-protected queues, batched queues, sharded queues and lock-free multi-producer queues.

The interesting conclusion is that no queue wins every workload.

In the reported benchmark, one design performed best for grouped single-thread work, another performed particularly well for burst-style traffic, and the sharded design greatly reduced high-fan-in p99 latency compared with the ordinary queues.

This destroys another common performance myth:

Lock-free automatically means fastest.

It does not.

Lock-free structures can still suffer from atomic contention, cache-line bouncing, CAS retries and memory-ordering costs.

Likewise, a mutex is not automatically slow.

If contention is low and the critical section is tiny, a simple mutex-based design can be excellent.

The right question is not "which data structure sounds the fastest?"

It is: which data structure is fastest for my actual workload?


24. Batching

Another classic optimization is batching.

Imagine your queue contains:

A
B
C
D
E

A simple consumer could repeatedly lock, take one item, and unlock.

A batched consumer might instead lock once, take A through E, and unlock once.

Now one lock operation handles multiple messages.

The paper reports that drain-side batching improves asynchronous throughput, and combining batching with memory pooling increased asynchronous throughput by about 2.46× in the measured experiment.

Again, the larger lesson is:

Reduce expensive work by amortizing it across multiple operations.

This idea appears everywhere: network packet batching, database transactions, GPU kernels, disk I/O, logging and message queues.


25. Production and Backtesting Can Use the Same Code

One of the less flashy but very interesting architectural ideas in the paper concerns simulation.

Trading firms test strategies using historical market data.

This is called backtesting.

During live trading, packets come from the exchange network. During backtesting, packets come from recorded history.

If the trading system is designed carefully, the actors do not need to know where the message came from.

Historical data and live data can execute the same strategy code.

Because grouped actors receive messages in a single ordered sequence, the system can also become more deterministic: replaying the same event sequence can reproduce the same actor behaviour, assuming external sources such as clocks and randomness are also controlled.

This is incredibly useful for debugging.

Instead of hoping a bug happens again, you can replay the recorded market sequence.

Ideally, the same input leads to the same processing and the same bug.


26. Does This Mean Every Actor System Should Use fast_send?

No.

This is extremely important.

fast_send works best for co-located actors, meaning actors inside the same address space.

If Actor A lives on Computer 1 and Actor B lives on Computer 2, you obviously cannot simply execute Actor B's C++ handler on Computer 1.

The network must be involved.

The paper explicitly limits fast_send to co-located actors.


27. Contention Can Also Break the Idea

Imagine 32 threads all do:

fast_send(orderBook, message);

at the same time.

There is still only one Order Book Actor.

Only one caller can have exclusive execution at a time.

So they begin waiting.

Now the synchronous approach may become worse than simply placing messages into a queue.

The paper explicitly identifies heavily contended "hot actors" as a limitation. Under enough contention, asynchronous delivery can be preferable because producers can enqueue work and continue instead of blocking.

This is another important systems principle:

An optimization that works beautifully at low contention may become terrible under high contention.

28. The Benchmark Has Limits

We should also be careful with the headline:

3370 ns → 30 ns

It does not prove:

fast_send is always 110× faster

The microbenchmark uses a sequential scenario with one message in flight.

The paper itself notes that saturated throughput across varying contention levels, chain depths and message sizes still requires further evaluation.

That is how benchmarks should be read.

Instead of asking "what number should I memorize?", ask "what mechanism explains the number?"

Here the mechanism is clear:

3370 ns
├── cross-thread communication
├── queue
├── wakeup
└── synchronization

90 ns
├── same thread
└── queue remains

30 ns
├── same thread
└── queue removed

That is the useful knowledge.


29. The Most Important Lesson of the Entire Paper

Many beginners think high-performance programming looks like this:

inline constexpr
__attribute__((always_inline))
std::atomic
AVX512
lock_free

Those tools can matter.

But some of the biggest performance improvements come from architecture.

Before optimizing ten instructions, ask:

Why does this operation cross a thread?
Why are we allocating memory?
Why does this message require a queue?
Why does this component live on another core?
Why does this data move between caches?
Why are we doing this work at all?

Removing an expensive operation entirely is usually more powerful than making that expensive operation 10% faster.

The paper is a good example.

The main optimization was not "make the queue 10% faster."

It was: do we need the queue here at all?


30. The Four Main Optimizations

If you want to remember the entire paper, remember these four ideas.

1. fast_send

Instead of Actor → queue → another thread → Actor, execute the receiver immediately when the actors are local and synchronous execution is appropriate.

2. Actor Groups

Instead of Decoder, Book and Strategy living on different cores, consider Decoder → Book → Strategy on one core when those operations form a naturally sequential stage.

3. Different Queues for Different Workloads

Do not assume one queue design is best everywhere. Choose based on number of producers, burstiness, contention and latency requirements.

4. Memory Pools

Instead of repeatedly doing new and delete on a latency-critical path, preallocate reusable storage when appropriate. This can especially help reduce unpredictable tail latency.


31. Five C++ Concepts Hidden Inside This Paper

Even if you never work in HFT, this paper teaches several useful C++ and systems ideas.

1. thread_local

Each thread can maintain its own state without locking against other threads.

2. RAII locking

Objects such as std::scoped_lock and std::unique_lock can manage lock lifetime safely.

3. Stack vs heap allocation

A stack object such as Message message; can be far more predictable than constantly doing new Message; on a hot path.

4. Cache locality

Keeping dependent work on one core can reduce movement of data between CPU caches.

5. Measure real workloads

A data structure that wins a synthetic lock-free benchmark might lose badly with your actual producer/consumer pattern.


32. What Should a College Student Take Away?

You do not need to understand CME protocols or professional trading to understand this paper.

The core idea is actually simple.

Imagine three students completing a worksheet.

Design A

Student A writes something.

A puts the page into a box.

Someone carries the box to Student B.

B takes the page out.

B writes something.

The page goes into another box.

Someone carries it to Student C.

C finishes the work.

Design B

The three students sit beside one another.

A finishes and immediately hands the paper to B.

B finishes and immediately hands it to C.

The actual calculation did not change.

The communication system changed.

Computers work similarly.

Sometimes moving work between workers costs more than the work.


33. The Entire Paper in One Diagram

                    NORMAL ACTOR MODEL

Actor A
   ↓
allocate message
   ↓
mailbox
   ↓
synchronization
   ↓
wake another thread
   ↓
Actor B
   ↓
reply mailbox
   ↓
wake Actor A

Round trip ≈ 3370 ns
in the paper's microbenchmark

                         ↓

                    ACTOR GROUP

Actor A
   ↓
mailbox
   ↓
Actor B

same thread

Round trip ≈ 90 ns

                         ↓

                     FAST_SEND

Actor A
   ↓
exclusive actor access
   ↓
Actor B handler runs immediately
   ↓
return value

same thread
no mailbox hop

Round trip ≈ 30 ns

34. The Entire HFT Pipeline in One Diagram

                   CME EXCHANGE

                        ↓

                 Network packets

                        ↓

                 Socket receiver
                        |
                        | asynchronous
                        ↓
                  Processing queue

                        ↓

        ┌──────────────────────────┐
        │       ONE CORE           │
        │                          │
        │   Decode market data     │
        │          ↓               │
        │     Update book          │
        │          ↓               │
        │    Run strategy          │
        │                          │
        │      fast_send           │
        └──────────────────────────┘

                        ↓
                   trading signal

                        |
                        | asynchronous
                        ↓

                  Order handler

                        ↓

                      CME

The cleverness is not "never use threads."

It is:

Use thread boundaries only where independent execution is worth their cost.

35. Final Takeaway

The research paper starts with a common assumption:

Actor systems are too slow for high-frequency trading.

Traditional actor systems often involve messages, mailboxes, threads, scheduling and context switching.

Those operations can indeed be expensive.

But the paper argues that those costs are not necessarily fundamental to actor-style state ownership when the actors live inside the same process.

Its solution combines:

synchronous fast_send
+
actor grouping
+
workload-specific queues
+
memory pools

to preserve much of the clean actor programming model while reducing communication overhead on latency-critical paths.

The benchmark that captures the idea is:

Cross-thread actors:       ~3370 ns
Same-thread actors:           90 ns
Same-thread fast_send:       ~30 ns

But do not memorize those numbers.

Memorize this:

Crossing a thread can cost much more than performing the actual work.

And remember an even broader lesson:

High-performance C++ is often less about writing clever C++ and more about understanding what the hardware and operating system are actually doing.

Before reaching for another thread, another queue, another lock-free container, or another complicated abstraction, ask a simpler question:

Do I actually need this operation at all?

Sometimes the fastest code is the code you remove.


Paper

Adapting the Actor Model of Concurrency for High-Frequency Trading: Synchronous Message Delivery (`fast_send`) and a Tick-to-Book Latency Study

Vincent Maciejewski, September 2026.

The paper is a 31-page arXiv preprint and describes the open-source C++20 kaspar-hft implementation. Because it is a preprint, its results should be understood as the author's reported measurements rather than as universally applicable performance numbers.

Likes are saved privately in your browser. Public discussion is hosted on GitHub so comments are shared across readers.

← Back to blog