VeztaVezta
Architecture

Caching & Queues

Redis cache strategy and BullMQ job scheduling

Vezta uses Redis for two purposes: application-level caching of frequently accessed data, and BullMQ job queues for all background processing. Both are backed by the same Redis instance running as a Docker container (vezta-production-redis in production, vezta-staging-redis on staging).

Redis Cache

The RedisCacheService (in src/common/cache/) provides a global caching layer available to all modules. Cache invalidation happens when the underlying data changes -- for example, when a price-sync job writes new prices, the corresponding cache entries are refreshed.

Other Redis-backed concerns:

  • Idempotency keys for orders -- idempotency:{key} with 5-min TTL
  • Circuit breaker -- circuit-breaker:{exchange} with 5-min auto-reset TTL
  • Insider-signals dedup -- alert:dedup:{wallet}:{marketId} with 24h TTL
  • Insider-signals daily caps -- alert:count:*
  • Endpoint rate limiting -- sliding-window counters via RedisCacheService.increment()
  • Geo-block IP cache -- 24h TTL on the DFlow proxy guard

BullMQ Queues

BullMQ handles all background processing through named queues. Each queue is registered per-module via BullModule.registerQueue() in the module's imports array -- not at the application level.

Active Queues

QueueOwning modulePurposeWorker domain
market-syncmarketfull-sync, price-sync, orderbook-syncmarket
short-duration-syncmarket5m/15m fetch + expiremarket
price-history-backfillmarketHistorical candle backfillmarket
crypto-pricemarketPeriodic broadcast + 24h tick cleanup for CryptoPriceTick (tick inserts are done inline by PolymarketRtdsConnector on the worker-rtds container)market
catalog-syncmarketPeriodic catalog cleanupmarket
trade-syncmarketPolymarket / Kalshi trade ingestionmarket
signal-ingestionmonitorNews connectors (RSS, GDELT, NewsData, CryptoNews, Telegram, X)misc
signal-detectormonitorMarket-data-based signal detection (whale moves, smart money)market
news-syncmonitorNews article syncmisc
insider-signalsinsider-signalsFilter + score + dispatch (classify-trade)traders
insider-cluster-refreshinsider-signalsHourly market cluster rebuildtraders
top-traderleaderboardSync (every 10 min) + enrich (every 30 min)traders
tracker-refreshtrackerTracked-wallet refreshtraders
wallet-listwallet-listPre-loaded wallet list refreshmisc
wallet-profilewallet-profilePer-trader profile page enrichmentmisc
prediction-refreshai-predictionsLLM prediction regenerationtraders
spread-scannerarbitrageCross-platform price discrepancy detectionmisc
notificationsnotificationEmail + in-app notification dispatchmisc
pushpush-notificationExpo push deliverymisc
share-cardshare-cardAsync OG card renderingmisc
points / missions / referrals / rewardsrewardsPoints calc, mission progress, referral commissionsmisc
alertsalertsPrice alert evaluationmisc
safe-deploymentsub-walletPolygon Safe deployment monitoringmisc
deposit-confirmationaccountCross-chain deposit confirmationmisc
order-monitortradingStuck-order reconciliation, fill pollingmisc
tp-sltradingTake-profit / stop-loss monitoringmisc
copy-trade-execcopy-tradeCopy trade detection + executiontraders
sniper-monitorsniperSniper trigger monitoringmisc
counter-trade-resetcounter-tradeDaily PnL/count resetmisc
resolution-payouttradingResolution event payout reconciliationmisc
data-archivalanalyticsOld-row archivalmisc
redis-cleanuphealthPeriodic key cleanupmisc
agent-request / agent-resume / agent-summarizeagentLive agent chat loop + conversation summarizer (Redis Streams)ai
research-requestresearchStructured market-research / probability estimationai
sports-stats / prematch-remindersports-stats / sports-alertsESPN live scores, standings, prematch reminders, resolutionsports
combocomboMulti-leg combo catalog sync, eligibility, settlementcombos
telegram-signalstelegram-signalsSmart-money signal publishing + settlement watchertraders
holders-sync / match-syncmarketHolders sync + cross-platform market matchingmarket
standings / weatherstandings / weatherWorld Cup standings snapshots, temperature marketsmisc

Worker Domain Routing

Each queue is bound to one of six job-bearing worker domains: traders, market, misc, sports, ai, or combos. (The websocket and rtds containers run persistent connectors, not BullMQ queues.) The matching container (e.g. vezta-production-worker-traders) is the only consumer for that domain.

  • backgroundOnly([Processor, Scheduler], '<domain>') excludes providers from DI on sibling workers, so BullMQ never registers a competing consumer
  • shouldRunInDomain('<domain>') is checked inside onModuleInit for setInterval-based services and WS connectors

The vezta-production-api container runs none of these workers -- it is HTTP + WebSocket only.

Scheduling Pattern

Modules that need repeatable jobs implement OnModuleInit and follow a consistent pattern:

async onModuleInit() {
  // 1. Clean old repeatable jobs to avoid duplicates
  const existing = await this.queue.getRepeatableJobs();
  for (const job of existing) {
    await this.queue.removeRepeatableByKey(job.key);
  }

  // 2. Register new repeatable jobs
  await this.queue.add('full-sync', {}, {
    repeat: { every: 5 * 60 * 1000 },
  });

  await this.queue.add('price-sync', {}, {
    repeat: { every: 60 * 1000 },
  });

  // 3. Optionally trigger an immediate run
  await this.queue.add('full-sync', {}, {
    jobId: 'initial-sync',
  });
}

Some modules (counter-trade, copy-trade) also use native setInterval for simpler polling. Prefer BullMQ for new schedulers -- better reliability, retry handling, and observability.

Processors

Each queue has a corresponding processor class decorated with @Processor('queue-name') that handles job execution:

@Processor('market-sync')
export class MarketSyncProcessor {
  @Process('full-sync')
  async handleFullSync(job: Job) {
    // Fetch markets, normalize, upsert...
  }

  @Process('price-sync')
  async handlePriceSync(job: Job) {
    // Fetch prices, write snapshots...
  }
}

Redis Configuration

BullModule.forRoot() in app.module.ts uses REDIS_HOST and REDIS_PORT environment variables (not REDIS_URL). These default to localhost:6379 if unset. In Docker, REDIS_HOST must be set to the container name (vezta-production-redis on prod, vezta-staging-redis on staging). If REDIS_HOST / REDIS_PORT are unset, getRedisConnection() falls back to REDIS_URL, then to localhost:6379.

Connection Details

EnvironmentREDIS_HOSTREDIS_PORT
Local developmentlocalhost6379
Production VMvezta-production-redis6379
Staging VMvezta-staging-redis6379

Both the cache and BullMQ queues share the same Redis instance. The Redis container has health checks (redis-cli ping, 10s interval, 3 retries); the API container depends on Redis being healthy before starting.

On this page