Performance Optimization

Strategies and techniques to optimize your Quub Exchange integration for speed, scalability, and efficiency.

Table of Contents

  1. API Performance
  2. Caching Strategies
  3. Database Optimization
  4. WebSocket Optimization
  5. Load Testing
  6. Monitoring & Profiling
  7. Scalability Patterns

API Performance

Connection Pooling

import { Agent as HttpsAgent } from "https";
import { Agent as HttpAgent } from "http";

// Configure connection pooling
const httpsAgent = new HttpsAgent({
  keepAlive: true,
  keepAliveMsecs: 30000,
  maxSockets: 50, // Max concurrent connections
  maxFreeSockets: 10, // Keep 10 idle connections
  timeout: 60000, // 60 second timeout
  scheduling: "lifo", // Last In First Out for better cache locality
});

const quubClient = new QuubClient({
  apiKey: process.env.QUUB_API_KEY,
  apiSecret: process.env.QUUB_API_SECRET,
  httpAgent: httpsAgent,
});

// Monitor connection usage
setInterval(() => {
  console.log("Connection Pool Stats:", {
    sockets: Object.keys(httpsAgent.sockets).length,
    freeSockets: Object.keys(httpsAgent.freeSockets).length,
    requests: Object.keys(httpsAgent.requests).length,
  });
}, 60000);

Request Batching

class BatchRequestManager {
  constructor(quubClient, options = {}) {
    this.client = quubClient;
    this.batchSize = options.batchSize || 50;
    this.batchDelay = options.batchDelay || 100; // ms
    this.queue = [];
    this.processing = false;
  }

  async getOrder(orderId) {
    return new Promise((resolve, reject) => {
      this.queue.push({ orderId, resolve, reject });

      if (!this.processing) {
        setTimeout(() => this.processBatch(), this.batchDelay);
        this.processing = true;
      }
    });
  }

  async processBatch() {
    if (this.queue.length === 0) {
      this.processing = false;
      return;
    }

    const batch = this.queue.splice(0, this.batchSize);
    const orderIds = batch.map((item) => item.orderId);

    try {
      // Fetch all orders in one request
      const orders = await this.client.exchange.getOrders({
        orderIds: orderIds.join(","),
      });

      // Resolve each promise
      batch.forEach((item) => {
        const order = orders.find((o) => o.orderId === item.orderId);
        if (order) {
          item.resolve(order);
        } else {
          item.reject(new Error(`Order ${item.orderId} not found`));
        }
      });
    } catch (error) {
      // Reject all promises in batch
      batch.forEach((item) => item.reject(error));
    }

    // Process next batch
    if (this.queue.length > 0) {
      setTimeout(() => this.processBatch(), this.batchDelay);
    } else {
      this.processing = false;
    }
  }
}

// Usage
const batchManager = new BatchRequestManager(quubClient);

// These will be batched into a single request
const [order1, order2, order3] = await Promise.all([
  batchManager.getOrder("order_1"),
  batchManager.getOrder("order_2"),
  batchManager.getOrder("order_3"),
]);

Parallel Requests

// ✅ GOOD: Parallel independent requests
async function loadDashboard() {
  const start = Date.now();

  const [markets, balances, openOrders, recentTrades] = await Promise.all([
    quubClient.exchange.getMarkets(),
    quubClient.custodian.getBalances(),
    quubClient.exchange.getOrders({ status: "open" }),
    quubClient.exchange.getTrades({ limit: 10 }),
  ]);

  console.log(`✅ Dashboard loaded in ${Date.now() - start}ms`);

  return { markets, balances, openOrders, recentTrades };
}

// ❌ BAD: Sequential requests
async function loadDashboardSlow() {
  const start = Date.now();

  const markets = await quubClient.exchange.getMarkets();
  const balances = await quubClient.custodian.getBalances();
  const openOrders = await quubClient.exchange.getOrders({ status: "open" });
  const recentTrades = await quubClient.exchange.getTrades({ limit: 10 });

  console.log(`⏱️ Dashboard loaded in ${Date.now() - start}ms`);

  return { markets, balances, openOrders, recentTrades };
}

// Benchmark: Parallel is typically 4-5x faster

Request Compression

import compression from "compression";
import axios from "axios";

// Enable request/response compression
const httpClient = axios.create({
  headers: {
    "Accept-Encoding": "gzip, deflate, br",
  },
  decompress: true,
});

// For Express API
app.use(
  compression({
    level: 6, // Compression level (0-9)
    threshold: 1024, // Only compress responses > 1KB
    filter: (req, res) => {
      if (req.headers["x-no-compression"]) {
        return false;
      }
      return compression.filter(req, res);
    },
  })
);

Caching Strategies

Multi-Layer Caching

import NodeCache from "node-cache";
import Redis from "ioredis";

class MultiLayerCache {
  constructor() {
    // L1: In-memory cache (fast, limited size)
    this.l1Cache = new NodeCache({
      stdTTL: 60,
      maxKeys: 1000,
    });

    // L2: Redis cache (shared, larger capacity)
    this.l2Cache = new Redis({
      host: process.env.REDIS_HOST,
      port: process.env.REDIS_PORT,
      maxRetriesPerRequest: 3,
    });
  }

  async get(key) {
    // Try L1 cache first
    let value = this.l1Cache.get(key);
    if (value) {
      console.log("✅ L1 cache hit:", key);
      return value;
    }

    // Try L2 cache
    const cached = await this.l2Cache.get(key);
    if (cached) {
      console.log("✅ L2 cache hit:", key);
      value = JSON.parse(cached);

      // Populate L1 cache
      this.l1Cache.set(key, value);
      return value;
    }

    console.log("❌ Cache miss:", key);
    return null;
  }

  async set(key, value, ttl = 60) {
    // Set in both caches
    this.l1Cache.set(key, value, ttl);
    await this.l2Cache.setex(key, ttl, JSON.stringify(value));
  }

  async invalidate(pattern) {
    // Invalidate L1
    const keys = this.l1Cache.keys();
    keys
      .filter((k) => k.startsWith(pattern))
      .forEach((k) => this.l1Cache.del(k));

    // Invalidate L2
    const l2Keys = await this.l2Cache.keys(`${pattern}*`);
    if (l2Keys.length > 0) {
      await this.l2Cache.del(...l2Keys);
    }
  }
}

// Usage
const cache = new MultiLayerCache();

async function getMarketData(symbol) {
  const cacheKey = `market:${symbol}`;

  let data = await cache.get(cacheKey);
  if (!data) {
    data = await quubClient.exchange.getMarket(symbol);
    await cache.set(cacheKey, data, 5); // 5 second TTL
  }

  return data;
}

Smart Cache Invalidation

class SmartCacheManager {
  constructor(cache) {
    this.cache = cache;
    this.setupEventListeners();
  }

  setupEventListeners() {
    // Invalidate on order events
    quubClient.on("order.created", (order) => {
      this.invalidateOrderCache(order);
    });

    quubClient.on("order.filled", (order) => {
      this.invalidateOrderCache(order);
      this.invalidateBalanceCache(order.userId);
      this.invalidateMarketCache(order.symbol);
    });

    quubClient.on("order.cancelled", (order) => {
      this.invalidateOrderCache(order);
    });
  }

  invalidateOrderCache(order) {
    this.cache.invalidate(`orders:${order.userId}`);
    this.cache.invalidate(`order:${order.orderId}`);
  }

  invalidateBalanceCache(userId) {
    this.cache.invalidate(`balance:${userId}`);
  }

  invalidateMarketCache(symbol) {
    this.cache.invalidate(`market:${symbol}`);
  }
}

Cache-Aside Pattern

class CacheAsideRepository {
  constructor(quubClient, cache) {
    this.client = quubClient;
    this.cache = cache;
  }

  async getMarket(symbol) {
    const cacheKey = `market:${symbol}`;

    // Try cache
    let market = await this.cache.get(cacheKey);
    if (market) return market;

    // Cache miss - fetch from API
    market = await this.client.exchange.getMarket(symbol);

    // Store in cache
    await this.cache.set(cacheKey, market, 5);

    return market;
  }

  async getMarkets() {
    const cacheKey = "markets:all";

    let markets = await this.cache.get(cacheKey);
    if (markets) return markets;

    markets = await this.client.exchange.getMarkets();
    await this.cache.set(cacheKey, markets, 30);

    return markets;
  }
}

Database Optimization

Indexing Strategy

-- Orders table indexes
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC);
CREATE INDEX idx_orders_symbol_created ON orders(symbol, created_at DESC);
CREATE INDEX idx_orders_status_created ON orders(status, created_at DESC);
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

-- Composite index for common query
CREATE INDEX idx_orders_user_symbol_status
  ON orders(user_id, symbol, status, created_at DESC);

-- Partial index for active orders only
CREATE INDEX idx_orders_active
  ON orders(user_id, created_at DESC)
  WHERE status IN ('open', 'pending');

-- Transactions table
CREATE INDEX idx_transactions_user_time ON transactions(user_id, created_at DESC);
CREATE INDEX idx_transactions_type_time ON transactions(type, created_at DESC);
CREATE INDEX idx_transactions_wallet ON transactions(wallet_id, created_at DESC);

Query Optimization

// ✅ GOOD: Efficient pagination
async function getRecentOrders(userId, page = 1, limit = 50) {
  const offset = (page - 1) * limit;

  const orders = await db.query(
    `
    SELECT id, symbol, side, type, quantity, price, status, created_at
    FROM orders
    WHERE user_id = $1
      AND status IN ('open', 'filled')
    ORDER BY created_at DESC
    LIMIT $2 OFFSET $3
  `,
    [userId, limit, offset]
  );

  return orders.rows;
}

// ❌ BAD: Loading all orders into memory
async function getRecentOrdersSlow(userId) {
  const orders = await db.query(
    `
    SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC
  `,
    [userId]
  );

  return orders.rows.slice(0, 50);
}

// ✅ GOOD: Cursor-based pagination (better for large datasets)
async function getOrdersCursor(userId, cursor = null, limit = 50) {
  const query = cursor
    ? `SELECT * FROM orders
       WHERE user_id = $1 AND created_at < $2
       ORDER BY created_at DESC LIMIT $3`
    : `SELECT * FROM orders
       WHERE user_id = $1
       ORDER BY created_at DESC LIMIT $2`;

  const params = cursor ? [userId, cursor, limit] : [userId, limit];
  const result = await db.query(query, params);

  return {
    orders: result.rows,
    nextCursor:
      result.rows.length > 0
        ? result.rows[result.rows.length - 1].created_at
        : null,
  };
}

Connection Pooling

import { Pool } from "pg";

// Configure PostgreSQL connection pool
const pool = new Pool({
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,

  // Pool configuration
  max: 20, // Maximum connections
  min: 5, // Minimum connections
  idleTimeoutMillis: 30000, // Close idle connections after 30s
  connectionTimeoutMillis: 2000,

  // Performance
  statement_timeout: 30000, // Query timeout
  query_timeout: 30000,

  // Monitoring
  log: (msg) => console.log("DB Pool:", msg),
});

// Monitor pool health
setInterval(() => {
  console.log("Pool Status:", {
    total: pool.totalCount,
    idle: pool.idleCount,
    waiting: pool.waitingCount,
  });
}, 60000);

// Graceful shutdown
process.on("SIGTERM", async () => {
  await pool.end();
  console.log("Database pool closed");
});

WebSocket Optimization

Message Batching

class OptimizedWebSocketClient {
  constructor(quubClient) {
    this.client = quubClient;
    this.messageBuffer = [];
    this.batchSize = 100;
    this.batchInterval = 100; // ms

    this.startBatching();
  }

  async connect() {
    this.ws = await this.client.exchange.connectWebSocket();

    this.ws.subscribe("market.ticker", ["BTC-USD", "ETH-USD"], (message) => {
      this.bufferMessage(message);
    });
  }

  bufferMessage(message) {
    this.messageBuffer.push(message);

    if (this.messageBuffer.length >= this.batchSize) {
      this.processBatch();
    }
  }

  startBatching() {
    setInterval(() => {
      if (this.messageBuffer.length > 0) {
        this.processBatch();
      }
    }, this.batchInterval);
  }

  processBatch() {
    const batch = this.messageBuffer.splice(0, this.batchSize);

    // Process messages in bulk
    this.handleMessages(batch);
  }

  handleMessages(messages) {
    // Aggregate updates
    const latestPrices = new Map();

    for (const msg of messages) {
      latestPrices.set(msg.symbol, msg.price);
    }

    // Update UI once with all changes
    this.updateUI(Array.from(latestPrices.entries()));
  }
}

Selective Subscriptions

// ✅ GOOD: Subscribe only to needed data
const ws = await quubClient.exchange.connectWebSocket();

// Only subscribe to specific symbols
ws.subscribe("market.ticker", ["BTC-USD", "ETH-USD"], handleTicker);

// Only subscribe to top of book (not full order book)
ws.subscribe("market.topOfBook", ["BTC-USD"], handleTopOfBook);

// ❌ BAD: Subscribe to everything
ws.subscribe("market.ticker", ["*"], handleTicker); // All symbols!
ws.subscribe("market.orderbook", ["BTC-USD"], handleOrderBook); // Full book!

Load Testing

API Load Testing

import autocannon from "autocannon";

async function loadTestApi() {
  const token = await getAuthToken();

  const result = await autocannon({
    url: "https://api.quub.fi/v1/markets",
    connections: 100, // Concurrent connections
    duration: 30, // Test duration in seconds
    pipelining: 10, // Requests per connection

    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },

    requests: [
      {
        method: "GET",
        path: "/v1/markets",
      },
      {
        method: "GET",
        path: "/v1/markets/BTC-USD",
      },
      {
        method: "POST",
        path: "/v1/orders",
        body: JSON.stringify({
          symbol: "BTC-USD",
          side: "buy",
          type: "limit",
          quantity: "0.001",
          price: "40000",
        }),
      },
    ],
  });

  console.log("Load Test Results:");
  console.log(`  Requests: ${result.requests.total}`);
  console.log(`  Duration: ${result.duration}s`);
  console.log(`  RPS: ${result.requests.average}`);
  console.log(`  Latency p50: ${result.latency.p50}ms`);
  console.log(`  Latency p99: ${result.latency.p99}ms`);
  console.log(`  Errors: ${result.errors}`);
}

Stress Testing

import { check, sleep } from "k6";
import http from "k6/http";

export const options = {
  stages: [
    { duration: "2m", target: 100 }, // Ramp up to 100 users
    { duration: "5m", target: 100 }, // Stay at 100 users
    { duration: "2m", target: 200 }, // Ramp up to 200 users
    { duration: "5m", target: 200 }, // Stay at 200 users
    { duration: "2m", target: 0 }, // Ramp down
  ],
  thresholds: {
    http_req_duration: ["p(95)<500"], // 95% requests < 500ms
    http_req_failed: ["rate<0.01"], // Error rate < 1%
  },
};

export default function () {
  const token = getAuthToken();

  // Get markets
  let res = http.get("https://api.quub.fi/v1/markets", {
    headers: { Authorization: `Bearer ${token}` },
  });
  check(res, { "markets status 200": (r) => r.status === 200 });

  sleep(1);

  // Get specific market
  res = http.get("https://api.quub.fi/v1/markets/BTC-USD", {
    headers: { Authorization: `Bearer ${token}` },
  });
  check(res, { "market status 200": (r) => r.status === 200 });

  sleep(1);
}

Monitoring & Profiling

Performance Metrics

import { Histogram, Counter } from "prom-client";

class PerformanceMonitor {
  constructor() {
    this.apiLatency = new Histogram({
      name: "api_request_duration_seconds",
      help: "API request duration",
      labelNames: ["method", "endpoint", "status"],
      buckets: [0.1, 0.5, 1, 2, 5, 10],
    });

    this.cacheHitRate = new Counter({
      name: "cache_requests_total",
      help: "Cache requests",
      labelNames: ["result"], // hit, miss
    });

    this.dbQueryDuration = new Histogram({
      name: "db_query_duration_seconds",
      help: "Database query duration",
      labelNames: ["query_type"],
      buckets: [0.01, 0.05, 0.1, 0.5, 1, 2],
    });
  }

  recordApiCall(method, endpoint, status, duration) {
    this.apiLatency.observe({ method, endpoint, status }, duration / 1000);
  }

  recordCacheHit() {
    this.cacheHitRate.inc({ result: "hit" });
  }

  recordCacheMiss() {
    this.cacheHitRate.inc({ result: "miss" });
  }
}

// Usage
const monitor = new PerformanceMonitor();

async function monitoredApiCall() {
  const start = Date.now();

  try {
    const result = await quubClient.exchange.getMarkets();
    const duration = Date.now() - start;

    monitor.recordApiCall("GET", "/markets", 200, duration);

    return result;
  } catch (error) {
    const duration = Date.now() - start;
    monitor.recordApiCall("GET", "/markets", error.statusCode || 500, duration);
    throw error;
  }
}

Scalability Patterns

Horizontal Scaling

// Using cluster module for multi-process
import cluster from "cluster";
import os from "os";

if (cluster.isMaster) {
  const numCPUs = os.cpus().length;

  console.log(`Master ${process.pid} starting ${numCPUs} workers...`);

  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on("exit", (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died, spawning new worker...`);
    cluster.fork();
  });
} else {
  // Worker process
  const app = createApp();
  app.listen(3000, () => {
    console.log(`Worker ${process.pid} started`);
  });
}

Message Queue for Async Processing

import Bull from "bull";

// Create job queue
const orderQueue = new Bull("orders", {
  redis: {
    host: process.env.REDIS_HOST,
    port: process.env.REDIS_PORT,
  },
});

// Process orders asynchronously
orderQueue.process(10, async (job) => {
  // 10 concurrent processors
  const { orderParams } = job.data;

  try {
    const order = await quubClient.exchange.createOrder(orderParams);
    return { success: true, orderId: order.orderId };
  } catch (error) {
    throw error; // Will retry automatically
  }
});

// Add order to queue
async function placeOrderAsync(orderParams) {
  const job = await orderQueue.add(
    { orderParams },
    {
      attempts: 3,
      backoff: {
        type: "exponential",
        delay: 2000,
      },
    }
  );

  return job.id;
}

Next Steps:


Performance Issues? Contact performance@quub.fi