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
| Queue | Owning module | Purpose | Worker domain |
|---|---|---|---|
market-sync | market | full-sync, price-sync, orderbook-sync | market |
short-duration-sync | market | 5m/15m fetch + expire | market |
price-history-backfill | market | Historical candle backfill | market |
crypto-price | market | Periodic broadcast + 24h tick cleanup for CryptoPriceTick (tick inserts are done inline by PolymarketRtdsConnector on the worker-rtds container) | market |
catalog-sync | market | Periodic catalog cleanup | market |
trade-sync | market | Polymarket / Kalshi trade ingestion | market |
signal-ingestion | monitor | News connectors (RSS, GDELT, NewsData, CryptoNews, Telegram, X) | misc |
signal-detector | monitor | Market-data-based signal detection (whale moves, smart money) | market |
news-sync | monitor | News article sync | misc |
insider-signals | insider-signals | Filter + score + dispatch (classify-trade) | traders |
insider-cluster-refresh | insider-signals | Hourly market cluster rebuild | traders |
top-trader | leaderboard | Sync (every 10 min) + enrich (every 30 min) | traders |
tracker-refresh | tracker | Tracked-wallet refresh | traders |
wallet-list | wallet-list | Pre-loaded wallet list refresh | misc |
wallet-profile | wallet-profile | Per-trader profile page enrichment | misc |
prediction-refresh | ai-predictions | LLM prediction regeneration | traders |
spread-scanner | arbitrage | Cross-platform price discrepancy detection | misc |
notifications | notification | Email + in-app notification dispatch | misc |
push | push-notification | Expo push delivery | misc |
share-card | share-card | Async OG card rendering | misc |
points / missions / referrals / rewards | rewards | Points calc, mission progress, referral commissions | misc |
alerts | alerts | Price alert evaluation | misc |
safe-deployment | sub-wallet | Polygon Safe deployment monitoring | misc |
deposit-confirmation | account | Cross-chain deposit confirmation | misc |
order-monitor | trading | Stuck-order reconciliation, fill polling | misc |
tp-sl | trading | Take-profit / stop-loss monitoring | misc |
copy-trade-exec | copy-trade | Copy trade detection + execution | traders |
sniper-monitor | sniper | Sniper trigger monitoring | misc |
counter-trade-reset | counter-trade | Daily PnL/count reset | misc |
resolution-payout | trading | Resolution event payout reconciliation | misc |
data-archival | analytics | Old-row archival | misc |
redis-cleanup | health | Periodic key cleanup | misc |
agent-request / agent-resume / agent-summarize | agent | Live agent chat loop + conversation summarizer (Redis Streams) | ai |
research-request | research | Structured market-research / probability estimation | ai |
sports-stats / prematch-reminder | sports-stats / sports-alerts | ESPN live scores, standings, prematch reminders, resolution | sports |
combo | combo | Multi-leg combo catalog sync, eligibility, settlement | combos |
telegram-signals | telegram-signals | Smart-money signal publishing + settlement watcher | traders |
holders-sync / match-sync | market | Holders sync + cross-platform market matching | market |
standings / weather | standings / weather | World Cup standings snapshots, temperature markets | misc |
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 consumershouldRunInDomain('<domain>')is checked insideonModuleInitforsetInterval-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
| Environment | REDIS_HOST | REDIS_PORT |
|---|---|---|
| Local development | localhost | 6379 |
| Production VM | vezta-production-redis | 6379 |
| Staging VM | vezta-staging-redis | 6379 |
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.