Engineering deep diveHow 50K bots trade without double orders
For engineers: the services, the order path, how work is split across servers, and the checks that keep real money safe.
Stack
- Services
- Node.jsExpressPM2 clusterPiscina
- Queues
- BullMQ256 shards
- Market data
- WebSocketRedis Pub/Sub
- State
- MongoDB replica setRedis
- Ops
- PrometheusGrafana
- Exchanges
- BinanceOKXBitgetBingX
Five services, one job each
- Socket ServerKeeps live price feeds from 4 exchanges and publishes one price snapshot every 300ms.
- Logic ServerRuns every bot's strategy and risk checks on each 300ms tick, in worker threads.
- DistributionThe admission gate: creates the order ID, blocks duplicates, rate-limits per user and exchange.
- Integration pool3 servers with whitelisted IPs that sign and send orders to the exchange.
- Process OrderApplies order status updates in order, finalises fills and frees the bot for its next trade.
One order, from price tick to fill
Each step is safe to retry. A repeat hits the same order ID and changes nothing.
- Socket ServerThe Socket Server publishes {SYMBOL: last} for each exchange every 300ms.
- BullMQA bot job lands on its fixed shard queue. The job ID is botId plus the tick, so the same tick is never queued twice.
- Logic ServerThe shard owner freezes a price snapshot and runs strategy plus risk in a Piscina worker pool.
- RedisBefore any side effect, it re-checks that it still owns the shard. If not, the decision is dropped.
- DistributionCreates the clientOrderId, rejects a second in-flight order for the same bot, and applies the rate limit.
- Integration poolUpserts the order by clientOrderId first, then sends the signed request to the exchange.
- Process OrderStatus events are queued by order ID. Old or out-of-order updates are dropped. A final status unlocks the bot.
Key decisions
Sharded queues with fenced ownership
- Problem
- One queue per server meant a dead server froze its bots, and adding servers meant moving bots by hand.
- Choice
- Bots hash into 256 fixed shards. Servers pick shards with weighted rendezvous hashing and hold each one with a 10-second Redis lock plus a fencing number.
- Why
- A dead server's shards move within seconds. Two servers can never both trade the same bot, because a stale owner fails the check before it sends anything.
Warm standby for every price feed
- Problem
- A dropped exchange WebSocket meant stale prices, and bots trading on old prices.
- Choice
- Each exchange has an active publisher, a warm standby that is connected but silent, and a cold standby. The active one holds a Redis lock and heartbeat.
- Why
- When the publisher dies, the warm standby takes the lock and keeps publishing with no reconnect gap.
One gate in front of every exchange
- Problem
- Exchanges ban accounts that send too fast, and a retried request could place a second real order.
- Choice
- Only Distribution creates order IDs. It keeps an active set of in-flight orders and rate-limits each user and exchange (20 per 10s, 100 per minute).
- Why
- Every retry carries the same ID, and MongoDB enforces it with a unique index. A repeat can never become a second order.
The ownership check, simplified
shard.worker.jssimplified sketch
async function tick(sid, jobs) {
const me = `${instId}#${fencing[sid]}`;
// soft check: don't burn CPU on a shard we may have lost
if (await redis.get(`logic:shard:owner:${sid}`) !== me) return;
const prices = snapshot(); // frozen for this 300ms tick
const decisions = await pool.run({ jobs, prices }); // strategy + risk, off the event loop
// hard check: never send if another server took the shard meanwhile
if (await redis.get(`logic:shard:owner:${sid}`) !== me) return;
for (const d of decisions) await distribution.post('/orders', d);
}
Scale
~50Ktrading bots
4exchanges behind one gateway
300msdecision tick
256shards across the logic servers
5,000customers
Reliability and operations
- Two checks per tickA soft check before computing saves CPU. A hard check before sending stops two owners.
- Back-pressureRising 429s, timeouts or event-loop delay shrink batches and slow the tick automatically.
- Gentle rebalancingShards move only when load is off by more than 10%, one or two at a time, with a cool-down.
- MonitoringEvery service exposes metrics to Prometheus, with Grafana dashboards and alerts.