Low-Latency Network Architectures

For High-Frequency Trading (HFT) and financial exchange systems, standard cloud networking introduces unacceptable latency. This file covers the three pillars of ultra-low-latency networking: exchange connectivity, multicast market data, and kernel bypass.

0/0 checks

Why Standard Cloud Networking Is Too Slow

Every layer a packet crosses adds latency — and the standard path stacks far more of them between the trading app and the exchange than an optimized path does. Flip between the two to see which layers disappear:

graph LR
    App["Trading app"] --> EC2["EC2/GCE"] --> VPCR["VPC router"] --> IGW["Internet GW"] --> ISP["ISP"] --> EXCH["Exchange"]
Latency: 5–50ms (internet) + kernel overhead (~50–200μs per hop). Six hops — three of them (Internet GW, ISP, the public internet itself) completely outside your control.
graph LR
    App2["Trading app"] --> NIC["NIC (DPDK)"] --> DX["Direct Connect / dedicated line"] --> EXCH2["Exchange colocation"]
Latency: 50–500μs end-to-end, sub-10μs for kernel-bypassed local ops. Four hops, none of them the public internet.

Every layer adds latency. HFT eliminates as many layers as possible.

The optimized HFT path still has 4 hops, not 1. So why is it so much faster than the standard path's 6 hops?


1. Exchange Connectivity

The Problem with Public Internet

Standard HTTPS/TCP over the public internet for trading has three fundamental problems:

  • Variable latency — congestion, routing changes, packet loss cause jitter (spikes from 1ms to 100ms)
  • Kernel overhead — each packet goes through the Linux TCP/IP stack (~10–50μs of CPU time)
  • Shared infrastructure — you're competing for bandwidth with other traffic

For context: a stock price can move in 10μs. A 5ms network jitter means you're acting on data that's 500,000 "ticks" stale.

A stock price can move in 10μs, and the public internet can jitter by 5ms. Roughly how many "ticks" stale is the data you'd be acting on during that spike?

AWS Direct Connect

Direct Connect is a dedicated private fiber from your on-prem/colo facility to AWS — bypassing the public internet entirely.

graph LR
    classDef hft fill:#e74c3c,stroke:#c0392b,color:#fff
    classDef aws fill:#FF9900,stroke:#e68a00,color:#fff
    classDef blue fill:#3498db,stroke:#2980b9,color:#fff
    classDef green fill:#2ecc71,stroke:#27ae60,color:#fff

    COLO["HFT Colo Facility<br/>(NSE/BSE/SGX data center)"]:::hft
    DX_LOC["Direct Connect Location<br/>(AWS PoP in same building<br/>or nearby colo)"]:::aws
    DX_GW["Direct Connect Gateway<br/>(AWS side)"]:::aws
    VGW["Virtual Private Gateway<br/>(VPC attachment)"]:::aws
    VPC["Trading VPC<br/>(order management, risk)"]:::blue
    EXCHANGE["Exchange Matching Engine"]:::green

    COLO -->|"1Gbps/10Gbps dedicated fiber<br/>no internet, no shared path"| DX_LOC
    DX_LOC -->|"private AWS backbone"| DX_GW
    DX_GW -->|BGP routes| VGW
    VGW -->|"private IPs only"| VPC
    COLO -->|"cross-connect in colo<br/>sub-100μs"| EXCHANGE

Key configuration points:

# Direct Connect connection (provisioned via AWS Console or partner)
# After physical fiber is in place:

# Create a Virtual Interface (VIF) — the logical BGP session
aws directconnect create-private-virtual-interface \
  --connection-id dxcon-xxxxxxxx \
  --new-private-virtual-interface '{
    "virtualInterfaceName": "trading-vif",
    "vlan": 100,
    "asn": 65000,
    "mtu": 9001,
    "authKey": "bgp-md5-key",
    "amazonAddress": "169.254.0.1/30",
    "customerAddress": "169.254.0.2/30",
    "virtualGatewayId": "vgw-xxxxxxxx"
  }'

# Verify BGP session is UP
aws directconnect describe-virtual-interfaces \
  --virtual-interface-id dxvif-xxxxxxxx \
  --query 'virtualInterfaces[0].bgpPeers[0].bgpStatus'
# → "up"

BGP Tuning for HFT

Standard BGP convergence time (when a route changes) is 30–90 seconds. That's catastrophic for trading. Tune it:

# On your BGP router (Cisco/Juniper/FRRouting):
neighbor 169.254.0.1 timers 3 9          # hello=3s, hold=9s (default: 60/180)
neighbor 169.254.0.1 timers connect 5    # retry on failure: 5s

# BFD (Bidirectional Forwarding Detection) — detects link failure in <1 second
neighbor 169.254.0.1 bfd
bfd interval 300 min_rx 300 multiplier 3 # 300ms intervals, 3 misses = 900ms failover

Active-Active Redundancy

For exchanges with strict uptime SLAs, run two Direct Connect connections from two different colo facilities (different physical buildings) in active-active mode:

graph TD
    ColoA["Colo A"] -->|DX| Loc1["AWS Direct Connect<br/>Location 1"]
    ColoB["Colo B"] -->|DX| Loc2["AWS Direct Connect<br/>Location 2"]
    Loc1 -->|both active,<br/>BGP ECMP load balancing| VPC["Trading VPC"]
    Loc2 -->|both active,<br/>BGP ECMP load balancing| VPC
# Direct Connect Gateway links BOTH VIFs to the same VPC
aws directconnect create-direct-connect-gateway \
  --direct-connect-gateway-name trading-dcg \
  --amazon-side-asn 64512

# Attach both VIFs to the gateway — AWS routes over both automatically
aws directconnect create-direct-connect-gateway-association \
  --direct-connect-gateway-id dcg-xxxxxxxx \
  --gateway-id vgw-xxxxxxxx

Put the tuned BGP timers, BFD, and this active-active pair together and here's what actually happens when a link dies:

1. Healthy. Both Direct Connect links are up. Each has its own BGP session, and BFD is exchanging hellos every 300ms on both.
2. A link fails. Colo A's fiber path (or its BGP peer) goes down. No more BFD hellos arrive on that session.
3. BFD detects it. After 3 missed 300ms intervals (~900ms) BFD declares the session dead — versus 30–90 seconds for unmodified BGP hold timers alone.
4. BGP withdraws the route. The failed path drops out of the router's ECMP set for the trading VPC.
5. Traffic shifts. Everything flows over the surviving Direct Connect connection (Colo B) with no manual intervention. Total failover: under a second.

Why does BFD cut failover time from 30–90 seconds down to under a second, when it's running alongside BGP rather than replacing it?

On-Premises / Co-location Scenario (Non-AWS)

When colocated in the same building as the exchange (e.g., NSE's colo at Powai, BSE's at Goregaon):

graph TD
    Rack["Your server rack"] -->|10GbE fiber<br/>cross-connect| Switch["Exchange switch<br/>Layer 2 direct link<br/>1–10μs, no routers, no internet"]
    Server["Your server"] --> NIC1["NIC 1<br/>cross-connect to exchange<br/>(trading traffic)"]
    Server --> NIC2["NIC 2<br/>management network"]
    NIC2 --> Inet["Internet"] --> Cloud["Your cloud"]

Both AWS Direct Connect and a colo cross-connect skip the public internet entirely. Which one is actually lower latency, and why?


2. Multicast Routing for Market Data

What Multicast Is

UDP Multicast sends one packet that reaches many receivers simultaneously without the sender transmitting N copies. Every major exchange (NSE, BSE, CME, NASDAQ) distributes market data (tick data, order book updates) via multicast.

Server sends 1000 copies → 1000 clients. One packet per recipient — bandwidth cost scales 1000x with the number of clients.
Server sends to everyone → 1000 clients. Reaches every host on the segment whether it wants the feed or not — wastes bandwidth on uninterested receivers.
Server sends 1 copy → multicast group → 1000 clients. The network, not the sender, replicates the packet only to hosts that joined the group — bandwidth cost stays 1x no matter how many clients are listening.

Multicast uses IP addresses in the 224.0.0.0/4 range. Clients "join" a multicast group to receive packets for that feed.

A multicast feed goes from 1,000 subscribers to 10,000 overnight. How many copies of each packet does the sender now need to transmit?

The Problem in VPCs

By default, VPCs do not support IP multicast. AWS VPC is a software-defined network that doesn't forward multicast packets. If your market data feed publishes to 224.1.1.1:4000, your EC2 instances won't receive it without special configuration.

Solution: AWS Transit Gateway Multicast

AWS Transit Gateway (TGW) supports multicast routing — the only AWS service that does.

graph TD
    classDef aws fill:#FF9900,stroke:#e68a00,color:#fff
    classDef hft fill:#e74c3c,stroke:#c0392b,color:#fff
    classDef blue fill:#3498db,stroke:#2980b9,color:#fff
    classDef green fill:#2ecc71,stroke:#27ae60,color:#fff

    SOURCE["Market Data Feed Source<br/>(Exchange multicast publisher<br/>224.1.1.1:4000)"]:::hft
    TGW["Transit Gateway<br/>(multicast-enabled domain)"]:::aws
    VPC1["VPC: Order Management<br/>Subnet: 10.0.1.0/24"]:::blue
    VPC2["VPC: Risk Engine<br/>Subnet: 10.0.2.0/24"]:::blue
    VPC3["VPC: Analytics<br/>Subnet: 10.0.3.0/24"]:::green

    SOURCE -->|"UDP multicast packet"| TGW
    TGW -->|"replicated to group members"| VPC1
    TGW -->|"replicated to group members"| VPC2
    TGW -->|"replicated to group members"| VPC3

Setup:

# 1. Create multicast-enabled Transit Gateway
aws ec2 create-transit-gateway \
  --options '{
    "MulticastSupport": "enable",
    "DefaultRouteTableAssociation": "enable",
    "DefaultRouteTablePropagation": "enable"
  }'

# 2. Create a multicast domain
aws ec2 create-transit-gateway-multicast-domain \
  --transit-gateway-id tgw-xxxxxxxx \
  --options '{
    "Igmpv2Support": "enable",
    "StaticSourcesSupport": "disable",
    "AutoAcceptSharedAssociations": "disable"
  }'

# 3. Associate subnets with the multicast domain
aws ec2 associate-transit-gateway-multicast-domain \
  --transit-gateway-multicast-domain-id tgw-mcast-domain-xxxxxxxx \
  --transit-gateway-attachment-id tgw-attach-xxxxxxxx \
  --subnet-ids subnet-xxxxxxxx subnet-yyyyyyyy

# 4. Register the source (the EC2/appliance that publishes market data)
aws ec2 register-transit-gateway-multicast-group-sources \
  --transit-gateway-multicast-domain-id tgw-mcast-domain-xxxxxxxx \
  --group-ip-address 224.1.1.1 \
  --network-interface-ids eni-xxxxxxxx    # ENI of the feed publisher

# 5. Register subscribers (instances that need the feed)
aws ec2 register-transit-gateway-multicast-group-members \
  --transit-gateway-multicast-domain-id tgw-mcast-domain-xxxxxxxx \
  --group-ip-address 224.1.1.1 \
  --network-interface-ids eni-aaa eni-bbb eni-ccc

Step through what those five commands actually build, in order:

1. Create a multicast-enabled Transit Gateway. Multicast support is off by default on a TGW — it has to be explicitly enabled.
2. Create a multicast domain. With IGMPv2 support turned on, so instances can dynamically join/leave groups instead of everything being statically registered.
3. Associate subnets with the domain. Only instances in an associated subnet can participate in this multicast domain at all.
4. Register the source. The ENI of the feed publisher, tied to a specific multicast group IP (e.g. 224.1.1.1).
5. Register subscribers. The ENIs of every instance that needs the feed. Each one still has to send its own IGMP join from inside the OS/application before it actually receives packets.

On each subscriber instance:

# Join multicast group (Linux)
ip maddress add 224.1.1.1 dev eth0

# Or via application (Python example)
import socket, struct

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', 4000))

# Join multicast group
mreq = struct.pack('4sL', socket.inet_aton('224.1.1.1'), socket.INADDR_ANY)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

while True:
    data, addr = sock.recvfrom(65535)
    process_tick(data)

IGMP — How Membership Works

IGMP (Internet Group Management Protocol) is how instances tell the network "I want to receive multicast group X":

  • IGMPv2 Join: instance sends a join report to the multicast group address
  • IGMPv2 Leave: instance leaves the group
  • TGW Querier: TGW periodically sends IGMP queries to learn which instances are still subscribed
# Verify TGW sees your group members
aws ec2 search-transit-gateway-multicast-groups \
  --transit-gateway-multicast-domain-id tgw-mcast-domain-xxxxxxxx \
  --filters Name=group-ip-address,Values=224.1.1.1

# On the instance, check group membership
ip maddress show dev eth0
# → inet  224.1.1.1

An instance stops caring about a multicast feed but its application never sends an explicit IGMPv2 Leave. How does TGW eventually find out it's no longer subscribed?


3. Kernel Bypass — DPDK

Why the Linux Kernel Is Too Slow

Every packet received by a standard Linux application goes through this path:

graph LR
    NIC["NIC hardware"] --> IRQ["interrupt"] --> KIH["kernel interrupt<br/>handler"] --> SOFT["softirq"] --> STACK["network stack<br/>(ip_rcv → tcp_rcv/udp_rcv)"] --> SB["socket buffer"] --> SYS["system call<br/>(recvfrom)"] --> APP["user space<br/>application"]

Total overhead: ~5–50μs per packet, unpredictable due to kernel scheduling.

For HFT, this is too slow and too variable. DPDK (Data Plane Development Kit) eliminates the kernel from the data path entirely.

How DPDK Works

graph LR
    NICA["NIC"] --> KIA["kernel interrupt"] --> KSA["kernel TCP/IP stack"] --> SOCKA["socket"] --> APPA["app"]
50μs+ per packet, and variable — the kernel scheduler decides when your app actually gets to run.
graph LR
    NICB["NIC"] --> PMD["DPDK poll mode driver (user space)"] --> APPB["app"]
Sub-1μs. The kernel never sees the packet at all — no interrupt, no context switch, nothing to wait on the scheduler for.

DPDK's poll-mode driver spins in a tight loop constantly checking the NIC instead of waiting for an interrupt. What's the tradeoff?

DPDK:

  1. Takes exclusive control of the NIC — the kernel never sees those packets
  2. Uses polling instead of interrupts — the CPU spins in a tight loop checking the NIC (trades CPU for latency)
  3. Uses huge pages for packet buffers to eliminate TLB misses
  4. Pins threads to specific CPU cores (CPU affinity) to eliminate context-switch jitter

Setup

# Install DPDK
apt-get install dpdk dpdk-dev

# Reserve huge pages (2MB pages, 1024 of them = 2GB)
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
mkdir /mnt/huge
mount -t hugetlbfs nodev /mnt/huge

# Bind a NIC to DPDK's vfio-pci driver (removes it from kernel)
# First identify the NIC's PCI address
dpdk-devbind --status
# → 0000:00:03.0 'Virtio network device' if=eth1 drv=virtio-pci unused=vfio-pci

# Load vfio-pci module
modprobe vfio-pci

# Bind the NIC to DPDK (kernel loses visibility of this NIC)
dpdk-devbind --bind=vfio-pci 0000:00:03.0

# Verify
dpdk-devbind --status
# → 0000:00:03.0 'Virtio network device' drv=vfio-pci unused=virtio-pci
1. Install DPDK. The library and poll-mode drivers (PMDs) for your NIC.
2. Reserve huge pages. 2MB pages instead of the default 4KB — fewer, bigger pages means far fewer TLB misses walking packet buffers.
3. Load vfio-pci and bind the NIC to it. This is the point of no return for the kernel — once bound, the kernel's network stack can no longer see or use this NIC at all.
4. Verify the bind. dpdk-devbind --status should now show the NIC's driver as vfio-pci instead of its original kernel driver.

DPDK Application Skeleton (C)

#include <rte_eal.h>
#include <rte_ethdev.h>
#include <rte_mbuf.h>

#define RX_RING_SIZE 1024
#define MBUF_POOL_SIZE 8191

int main(int argc, char *argv[]) {
    // Initialize DPDK EAL (sets up huge pages, CPU affinity, etc.)
    rte_eal_init(argc, argv);

    // Create memory pool for packet buffers
    struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create(
        "MBUF_POOL", MBUF_POOL_SIZE, 250, 0,
        RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id()
    );

    uint16_t port_id = 0;

    // Configure the Ethernet port
    struct rte_eth_conf port_conf = {0};
    rte_eth_dev_configure(port_id, 1, 1, &port_conf);

    // Setup RX queue
    rte_eth_rx_queue_setup(port_id, 0, RX_RING_SIZE,
        rte_eth_dev_socket_id(port_id), NULL, mbuf_pool);

    // Start device
    rte_eth_dev_start(port_id);

    // Poll loop — no interrupts, no kernel, just spin
    struct rte_mbuf *bufs[32];
    while (1) {
        // Receive up to 32 packets in one call
        uint16_t nb_rx = rte_eth_rx_burst(port_id, 0, bufs, 32);

        for (int i = 0; i < nb_rx; i++) {
            // Process packet directly from NIC buffer
            process_market_data_packet(bufs[i]);
            rte_pktmbuf_free(bufs[i]);
        }
        // No sleep — spins at full speed, consuming 100% of one core
    }
}

CPU Isolation for DPDK Cores

The polling core must never be preempted by the OS scheduler. Isolate it:

# In /etc/default/grub, add to GRUB_CMDLINE_LINUX:
# isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3

# This tells the kernel: "never schedule any tasks on cores 2 and 3"
# DPDK pins its polling threads to these isolated cores

# After reboot, verify
cat /sys/devices/system/cpu/isolated
# → 2-3

# Set CPU affinity for DPDK app (cores 2 and 3)
dpdk-app -l 2,3 --socket-mem=1024,0 -- [app args]

DPDK on AWS (ENA + EFA)

AWS Elastic Network Adapter (ENA) supports DPDK via the ena PMD driver. For ultra-low latency between EC2 instances, use Elastic Fabric Adapter (EFA):

# EFA-enabled instance types: c5n, hpc6a, p4d, trn1
# EFA bypasses the kernel TCP/IP stack for inter-instance traffic

# Install EFA driver
./efa_installer.sh

# Check EFA device
fi_info -p efa
# → provider: efa, fabric: EFA-fe80::..., name: efa_0-rdm

# DPDK with EFA: use the efa PMD
dpdk-app -l 0,1,2,3 \
  -a <efa-pci-addr> \
  --vdev='net_efa0' \
  --iova-mode=va \
  -- [app args]

EFA uses RDMA (Remote Direct Memory Access) — data is written directly from one machine's memory into another's NIC buffer, bypassing both kernels. Latency: ~2–4μs between instances in the same placement group.

RDMA (via EFA) writes data straight from one machine's memory into another machine's NIC buffer. Whose kernel is involved in that transfer?


Latency Budget — What Each Layer Costs

Layer                      Typical latency     HFT budget
────────────────────────────────────────────────────────
Internet routing           1–50ms              ✗ not acceptable
VPC software routing       10–100μs            borderline
Direct Connect (DX)        0.5–2ms (fiber)     ✓ for non-colo
Colo cross-connect         1–10μs              ✓ best option
Linux kernel TCP/IP stack  10–50μs/packet      borderline
DPDK (kernel bypass)       0.1–2μs/packet      ✓
RDMA/EFA                   2–4μs inter-host    ✓
Shared memory (same host)  0.05–0.1μs          ✓ fastest

Direct Connect shows a higher number (0.5–2ms) than VPC software routing (10–100μs) in this table, yet DX gets a ✓ and VPC routing only "borderline." What does the ✓ actually track — the raw number, or something else?


Summary

Problem Solution
Internet latency/jitter AWS Direct Connect or physical colo cross-connect
BGP failover too slow BFD (sub-second detection) + tuned BGP timers
VPC doesn't support multicast AWS Transit Gateway with multicast domain
Linux kernel adds per-packet overhead DPDK with poll-mode driver + CPU isolation
Inter-instance latency EFA (RDMA) + placement group
CPU scheduling jitter isolcpus + nohz_full + rcu_nocbs