ENGINEERING · CROSS-CUTTING SUBSYSTEM · 10-05-2026
Redis Architecture
Cross-cutting cache, coordination bus, and queue backbone — every namespace, TTL, pub/sub channel, Bull queue, lock pattern, and the cross-repo handoffs that bind the platform together.
70+
Key namespaces (backend)
1. At-a-glance
| Metric | Value |
| Redis libraries in use | 2 — redis v4 (node-redis) in 7 repos · ioredis in 2 repos |
| Connection target | process.env.TRADEIT_REDIS_URI · port 6379 · plaintext (no TLS) |
| Repos that read or write Redis | 8 — backend, admin-backend, socket-server, tradebot-server, inventory-parser, stats-manager, stock-manager, oauth2-server, buff-scrapper |
Helper modules in tradeit-backend | 14 under server/redis/ |
| Distinct key namespaces (backend) | 70+ across helpers, services, controllers, queues |
| Bull queue instances | 2 — tradeitQueue (main URI) + tradeitScreenshotQueue (separate URI) |
| Pub/sub channels | 5 main — tradestate, bot <botId>, itemReserved, image, canceledtrades |
| Distributed locks | 4 patterns — lockTrade:, indexLaneLock:, updateItemsStocks, syncItemPricesRunning |
Hard rule violations (KEYS) | 0 — no redis.keys() calls in any tradeit repo |
ElastiCache IaC tracked in tradeit-infra | NONE — provisioning lives outside git |
2. Purpose & boundaries
Redis serves four distinct responsibilities that share one ElastiCache instance:
- L2 cache — write-through caches with TTLs from 5s (rate limits) to 365d (AI-generated SEO), backed by MySQL/OpenSearch source-of-truth.
- Coordination bus — pub/sub channels carry trade-state events, bot commands, and item-reservation notifications between
tradeit-backend, tradeit-socket-server, and the bot fleet.
- Bull queue backbone —
tradeitQueue (main work) and tradeitScreenshotQueue (image processing) persist job state in the bull:* keyspace.
- Distributed coordination — atomic locks (SET NX), inventory mirrors (
sinv:*/cinv:*), rate-limit counters.
Owns
- Key naming conventions and TTLs
- Pub/sub channel definitions and payload shapes
- Bull queue topology (queue names, processor entry points)
- Distributed-lock TTLs and renewal semantics
Does NOT own
- Underlying ElastiCache cluster (provisioned outside
tradeit-infra)
- Bot daemon's text-protocol semantics on
bot <botId> (defined in tradebot-server)
- Express session storage internals (delegated to
connect-redis middleware)
- Eviction policy (configured at the cluster, not in code)
3. Process / runtime model
3.1 tradeit-backend — singleton client + on-demand attach
- File:
server/redis.js
Redis is a module-level singleton: Redis.init() lazily calls createClient({ socket: { port: 6379, host: process.env.TRADEIT_REDIS_URI }, legacyMode: false }) and returns the same connection for every caller.
- Bootstrap path:
server/index.js:79 calls await redis.connect() for the session-store connection (legacyMode: true); the main connection opens lazily on first Redis.init().
- Express middleware
attachRedis attaches the client to req.redis; on connection failure returns HTTP 500 Redis is offline — fail-fast, no graceful fallback.
- PM2 cluster mode: each worker holds its own pair (1 main + 1 session). Bull queues open additional connections (see §9).
3.2 Other repos
- tradeit-socket-server —
redis v4. One subscriber connection (src/socket.ts:57-65). Forwards pub/sub messages to Socket.IO rooms.
- tradeit-tradebot-server —
redis v4. Single connection used both as KV store (bot inventory caches) and as pub/sub publisher to bot <botId> and tradestate.
- tradeit-admin-backend —
redis v4 + connect-redis. Maintains its own ConfigurationService that does not coordinate invalidation with the backend.
- tradeit-stats-manager —
ioredis. Refreshes configuration:* from MySQL every 5 min via @Cron(EVERY_5_MINUTES), with no TTL.
- tradeit-stock-manager —
redis v4. @LockWithRedis('updateItemsStocks', 300) decorator.
- tradeit-buff-scrapper —
redis v4. Generic lock service wrapper.
- tradeit-oauth2-server —
redis v4 + connect-redis. login:<steamId>_<ihash> for OAuth2 nonce + sess:* for sessions.
- tradeit-inventory-parser —
redis v4 (NestJS RedisStore). Holds indexLaneLock:<gameId>:<lane> during the 10-minute alias-swap cycle.
No Redis usage: tradeit-login-server, tradeit-inventory-server, tradeit-tradebot-creator, tradeit-cms (Strapi).
3.3 Connection footprint at steady state
| Process | Connections per instance |
tradeit-backend (per PM2 worker) | 1 main + 1 session-store + 1 per Bull queue active in that worker |
tradeit-socket-server | 1 subscriber connection |
tradeit-tradebot-server (per bot worker) | 1 client + 1 subscriber to bot <botId> |
tradeit-stats-manager | 1 ioredis client |
| Other services | 1 connection each |
With ~400 bot workers + 4-8 backend PM2 workers + socket-server replicas, expect 500–700 client connections at peak.
4. Architecture
graph LR
subgraph "tradeit-backend"
BE_API["controllers / services
(70+ key namespaces)"]
BE_CFG["configurationService.js
L1 60s + L2 180s"]
BE_RES["reserveAssetRedis.js
list-based reservation"]
BE_TRADE["tradeRedis.js
locks + pub/sub"]
BE_BULL_MAIN["mainQueue.js
tradeitQueue (Bull)"]
BE_BULL_SS["screenshotQueue.js
tradeitScreenshotQueue (Bull)"]
end
subgraph "Redis (ElastiCache)"
R_KV[("KV namespaces
cinv:* sinv:* configuration:*
insight_* invsize* bot_acc_info ...")]
R_LIST[("Lists
reserved-assets_*
reserved-assets_ttl_*")]
R_PUBSUB[("Pub/Sub
tradestate · bot <id>
itemReserved · image · canceledtrades")]
R_BULL[("Bull keyspace
bull:tradeitQueue:*
bull:tradebotQueue:*")]
end
subgraph "tradeit-socket-server"
SOCK_SUB["sub.subscribe()
tradestate · canceledtrades
itemReserved"]
SOCK_IO["io.to(steamId).emit(...)"]
end
subgraph "tradeit-tradebot-server (~400 bots)"
BOT_KV["invsize* · pollData
botRefreshToken · inspectResult"]
BOT_SUB["sub.subscribe('bot <id>')"]
BOT_PUB["publish 'tradestate'
publish 'finishedtrades'
publish 'canceledtrades'"]
end
subgraph "tradeit-admin-backend"
ADMIN_CFG["configurationService.ts
writes configuration:*
(no DEL on edit — gap)"]
end
subgraph "tradeit-inventory-parser"
PARSE_LOCK["indexLaneLock:*
10-min alias-swap"]
end
BE_API --> R_KV
BE_CFG --> R_KV
BE_RES --> R_LIST
BE_RES --> R_PUBSUB
BE_TRADE --> R_KV
BE_TRADE --> R_PUBSUB
BE_BULL_MAIN --> R_BULL
BE_BULL_SS --> R_BULL
ADMIN_CFG -.->|"NO INVALIDATION"| R_KV
PARSE_LOCK --> R_KV
BOT_KV --> R_KV
BOT_PUB --> R_PUBSUB
BOT_SUB --> R_PUBSUB
R_PUBSUB --> SOCK_SUB
SOCK_SUB --> SOCK_IO
classDef gap fill:#2a1218,stroke:#f43f5e,stroke-width:2px,color:#fecdd3
class ADMIN_CFG gap
The dashed NO INVALIDATION edge is the single most damaging cache-coherency gap on the platform. tradeit-admin-backend writes configuration:<key> on admin edits but never publishes a DEL or pub/sub event, so tradeit-backend instances continue serving stale values for up to 180s (per-key TTL) or 360s worst-case for category-bundle reads. See Platform Configuration §6.
5. Inputs & outputs
5.1 Connection ingress
| Connector | Library | Connection options | File |
| tradeit-backend main | redis v4 | socket: { host: TRADEIT_REDIS_URI, port: 6379 } | server/redis.js:36-46 |
| tradeit-backend session | redis v4 (legacyMode) | Same host, separate connection | server/index.js:73-90 |
Bull tradeitQueue | bull v4.16.5 | redis://${TRADEIT_REDIS_URI}:6379 | server/queue/mainQueue.js:34 |
Bull tradeitScreenshotQueue | bull v4.16.5 | redis://${TI_SCREENSHOT_SKIN_REDIS_URI}:6379 | server/queue/screenshotQueue.js:24 |
| tradeit-socket-server | redis v4 | One subscriber connection | src/socket.ts:55-66 |
| tradeit-stats-manager | ioredis | REDIS_HOST:REDIS_PORT | src/redis/redis.service.ts |
5.2 Egress (consumers)
- HTTP responses — every cache hit on
getValueByKey, getCachedInventory, getInsightChart shaves a MySQL/OpenSearch round-trip from the request path.
- Socket.IO emits —
tradeit-socket-server translates tradestate and itemReserved pub/sub messages into Socket.IO events scoped to the user's steamId room (src/socket.ts:164).
- Bot daemon — bot workers subscribe to
bot <botId> and act on withdraw, sale, checkSteamGuard commands.
5.3 No HTTP control plane
There is no admin HTTP endpoint for invalidating Redis keys directly. Cache busting happens through service-level redis.del(key) calls embedded in write paths, Bull job removeReserve triggering lRem on the reservation lists, and the daily 01:00 UTC cron sweeping insight_keys:<gameId>.
6. Keyspace catalog
Grouped by namespace. TTLs are in seconds unless noted. Source paths rooted at tradeit-backend/ unless qualified.
6.1 Inventory & catalog
| Key | TTL | Ops | Purpose | Source |
sinv | persistent | GET | Aggregate site inventory snapshot | server/redis/inventoryRedis.js |
sinv:${botIndex}:compressed | persistent | GET | Per-bot inventory, zstd-compressed | server/redis/inventoryRedis.js |
cinv:${steamId}:${gameId}:compressed | persistent | GET, DEL | User inventory per game, compressed | server/redis/inventoryRedis.js |
cinv:${steamId}:lastRequest | DEL on demand | DEL | Invalidate user inventory cache | server/redis/inventoryRedis.js |
cinv:${steamId}:playedGames | DEL on demand | DEL | Invalidate played-games cache | server/redis/inventoryRedis.js |
hardInventoryRefresh:${steamId} | 600 | SETEX, GET | Throttle expensive Steam refreshes | server/redis/inventoryRedis.js |
globalItemInfo | persistent | GET | Item metadata catalog | server/redis/inventoryRedis.js |
skins_count:${appId} | 600 | SETEX, GET | Game skins inventory count | server/redis/categoryRedis.js:5-13 |
caskets730:${botId} | persistent | GET | CS:GO container metadata per bot | server/repository/containerItemsRepo.js |
containers:${botIndex} | n/a | MGET | Bulk-read of container IDs across bots | server/service/botsService.js:166+ |
6.2 Bot fleet state cross-repo: backend reads, tradebot-server writes
| Key | TTL | Ops | Purpose | Written by |
bot_acc_info:${botId} | persistent | SET, GET | Bot login credentials + metadata | server/redis/botAccountRedis.js:4-12 |
invsize440:${botId} | persistent | SET, GET | TF2 backpack metadata | tradeit-tradebot-server/src/index.ts:340 |
invsize730:${botId} | persistent | SET, GET | CS:GO inventory size | tradeit-tradebot-server/src/index.ts:2893 |
invsizeProtected730:${botId} | persistent | SET, GET | CS:GO trade-locked items | tradeit-tradebot-server/src/index.ts:2894 |
invsizeAll730:${botId} | persistent | SET, MGET | Aggregate CS:GO inventory | tradebot-server; read by botsService.js:166+ |
stayingalive:${botIndex} | n/a | MGET | Bot heartbeat, bulk-read | botsService.js:166+ |
pollData:${botId} | persistent | SET, GET | Bot poll state, written every 3 min | tradeit-tradebot-server/src/index.ts:348 |
botRefreshToken:${botId} | persistent | SET, GET, DEL | OAuth2 refresh token | tradeit-tradebot-server/src/index.ts:505 |
inspectResult:${assetId} | 20 | SETEX, GET | Float-inspection result | tradeit-tradebot-server/src/index.ts:308 |
6.3 Trades & reservations
| Key | TTL | Ops | Purpose | Source |
userLastTradeTime:${steamId} | persistent | SET, GET | Last trade timestamp per user | server/redis/tradeRedis.js |
userOpenTrades:${botId} | 900 | SET, GET | Recent trade timestamps per bot | server/redis/tradeRedis.js |
userTradeLock:${steamId} | persistent | HGETALL, HSET | Hash of locked assetIds | server/redis/tradeRedis.js |
lockTrade:${assetId} | 900 | SET NX, DEL | Atomic trade lock (NX guard) | server/redis/tradeRedis.js |
tradeDisabled | persistent | SET, GET | Global trade kill switch | server/redis/tradeRedis.js |
reserved-assets_${appId} | none (list) | LPUSH, LRANGE, LTRIM, LREM | Per-game reserved asset list | server/redis/reserveAssetRedis.js:5,16,30,35 |
reserved-assets_ttl_${appId} | none (list) | LPUSH, LRANGE | Parallel list of {assetId, expired} JSON blobs | server/redis/reserveAssetRedis.js:6-8 |
⚠️ Reserved-asset expiry is encoded inside the JSON payload (expired: Date.now() + 20*60*1000), not via Redis TTL. Lazy expiry is enforced when a reader scans the list. This pattern is the root cause of the must_not.terms query bottleneck addressed by the Reserved-Assets OS-field redesign (spec 2026-05-10-reserved-assets-os-field-design.md).
6.4 Configuration cache owned by Configuration deep-dive
| Key | TTL | Ops | Notes |
configuration:${key} | 180 | GET, SETEX, DEL | Per-key config string. Also written by tradeit-admin-backend/services/configurationService.ts:62,71 with no DEL on admin edit |
configurationCategory:${cat}:0 | 180 | GET, SETEX | Full-category fetch, plain |
configurationCategory:${cat}:1 | 180 | GET, SETEX | Hash-keyed (compact frontend payload) |
See Platform Configuration for the L1+L2 read path, the no-pub/sub-invalidation gap, and the 11 config categories.
6.5 Marketplace, billing, identity
| Key | TTL | Ops | Purpose | Source |
marketplace_config_cache:${key} | variable | GET, SETEX, DEL | Marketplace settings cache | server/service/marketplaceConfigService.js:29-107 |
stripe_customer_id:${steamId} | persistent | SET, GET | Steam ID → Stripe customer ID | server/redis/stripeRedis.js:1-20 |
stripeCountryIds | 21600 (6h) | GET, SETEX | Stripe-supported country list | server/controllers/user.js:672-682 |
steamLevel:${steamId} | 300 | GET, SETEX | Steam user level cache | server/service/userService.js:238-245 |
steam_trade_verification:${steamId} | persistent | SET, GET, DEL | Trade-offer verification artefact | server/redis/steamTradeVerificationRedis.js |
steam_trade_verification_token:${steamId} | persistent | SET, GET, DEL | Verification token | server/redis/steamTradeVerificationRedis.js |
steamGuardLastCheckTime:${steamId} | 600 | SETEX, GET | Steam Guard check throttle | server/redis/checkSteamGuardRedis.js |
steamGuardStatus:${steamId} | persistent | GET | Boolean status (1/0) | server/redis/checkSteamGuardRedis.js |
steamGuardEscrowDays:${steamId} | persistent | GET | Escrow days count | server/redis/checkSteamGuardRedis.js |
login:${steamId}_${ihash} | session-implicit | SET, DEL | OAuth2 login state (nonce + challenge) | tradeit-oauth2-server/src/guards/login.guard.ts:52-59 |
sess:* | 30d | full set | Express session via connect-redis | server/index.js:73-90 |
6.6 Coupons, rate limits, server status
| Key | TTL | Ops | Purpose | Source |
coupon:${steamId} | 10 | GET, SETEX | Coupon-claim rate limit (rejects within 3000ms) | server/service/couponService.js:348-353 |
searchInventory:${clientIp} | 5 | GET, SETEX | Search rate-limit per IP | server/controllers/inventory.js:604-611 |
server_status | 300 | SETEX, GET | Live health JSON | server/redis/serverStatusRedis.js |
server_status_stats:${key} | 259200 (3d) | SETEX, GET | Historical stats snapshot | server/redis/serverStatusRedis.js |
siteStatsCache | 300 | GET, SETEX | Aggregated server stats | server/service/serverStatusService.js:85-102 |
usercount | persistent | GET | Active user count writer unknown — see §14 | server/service/serverStatusService.js |
syncItemPricesRunning | 3600 | GET, SETEX, DEL | Singleton lock for price sync cron | server/controllers/cron.js:822-834 |
revert_trades_sync_${gameId}_lastId | persistent | GET, SET | Cron resume-cursor | server/controllers/cron.js:1529-1587 |
inventory_sync_${gameId}_lastScanId | persistent | GET, SET | Cron resume-cursor | server/controllers/cron.js:1754-1786 |
6.7 SEO & insights
| Key | TTL | Ops | Purpose |
seo_page_content:/${locale}${uri} | 600 | SET, GET | SEO page metadata cache |
seo_gift_content:${appId}:${giftId} | persistent | SET, GET | Gift page SEO metadata |
writestuff_published_uris | persistent (set) | SADD, SMEMBERS, SISMEMBER, DEL | Set of published URIs |
insight_chart:${gameId}:${slug} | 86400 | SETEX, GET | Chart data |
insight_cat:${gameId} | 86400 | SETEX, GET | Category overview |
insight_cat_slug:${gameId}:${key} | 86400 | SETEX, GET | Category-specific data |
insight_col:${gameId} | 86400 | SETEX, GET | Collection insights |
insight_items_in_container:${gameId}:${containerId} | 86400 | SETEX, GET | Container item listings |
insight_container:${gameId}:${containerId} | 86400 | SETEX, GET | Container metadata |
insight_item_stock:${gameId}:${itemId}:${context} | 600 | SETEX, GET | Item availability |
insight_ai_content:${locale}:${slug} | 31536000 (configurable, default 365d) | SETEX, GET | AI-generated SEO summary; configurable via wikiAiContentTtlInDay |
aiGenerating:insight_ai_content:${locale}:${slug} | 600 | SETEX | Generation lock |
insight_keys:${gameId} | 86400 | SADD, SMEMBERS | Set of all insight keys per game |
insightInvalidateKeys | variable | SADD, SMEMBERS, DEL | Pending-invalidation set, swept by daily cron at 01:00 UTC |
6.8 Admin operations
| Key | TTL | Ops | Purpose |
admin_balance_cache | persistent | SET, GET | Sum of admin balances |
admin_delete_reserved_items:${appId}:${assetId} | persistent | SET, GET, DEL | Track admin removal of reserved items |
6.9 Locks (cross-cutting pattern)
| Lock key | Holder | TTL | Pattern |
lockTrade:${assetId} | tradeit-backend trade flow | 900s | SET NX EX 900 then DEL on completion |
indexLaneLock:${gameId}:${laneKey} | tradeit-inventory-parser | ~60s, renewed at 50% | SET NX EX + Lua-EVAL CAS DEL on release |
updateItemsStocks | tradeit-stock-manager | 300s | @LockWithRedis decorator |
syncItemPricesRunning | tradeit-backend cron | 3600s | SETEX → DEL on success/failure |
aiGenerating:insight_ai_content:* | insightRedis AI generator | 600s | SETEX → DEL |
6.10 Bull keyspace
Bull stores its job state under the bull:<queueName>:* prefix automatically. Keys observed: id, wait, active, completed, failed, delayed, paused, priority, jobs:<id>, events, meta. No application code touches these directly; manage exclusively via the Bull API.
7. MySQL coupling
Redis keys most tightly coupled to MySQL:
| Redis key | MySQL source | Read path | Invalidation |
configuration:* | steamarbitrage.configurations | L1 → L2 → MySQL | Per-key DEL on backend; no DEL on admin edits |
configurationCategory:*:* | steamarbitrage.configurations | L2 → MySQL | TTL only (180s) |
caskets730:* | steamarbitrage.bot_caskets | Redis → MySQL fallback | Manual DEL on bot inventory change |
stripe_customer_id:* | steamarbitrage.users.stripe_customer_id | Redis → MySQL fallback | Persistent — re-set on user create |
revert_trades_sync_*_lastId | cron resume cursor | Redis only | Read-modify-write each cycle |
Redis is always cache or coordination, never the source of truth, except for ephemeral state (locks, rate limits, pub/sub-driven flows).
8. OpenSearch coupling
Redis indirectly affects OpenSearch through the reservation flow:
reserved-assets_${appId} list is read by tradeit-inventory-parser during the 10-min alias-swap to filter the inventory snapshot. The new design replaces this dependency with an indexed reservedUntil field on the inventory document itself (see 2026-05-10-reserved-assets-os-field-design.md), reducing the keyspace dependency.
No direct Redis ↔ OpenSearch sync. OpenSearch indexes are rebuilt from MySQL and Redis is consulted as an overlay during query construction.
9. Bull queues / cron / pub-sub continuations
9.1 Bull queues
| Queue | Repo | Redis URI | Tasks | Removal |
tradeitQueue | tradeit-backend | ${TRADEIT_REDIS_URI}:6379 | tradeBonus, logSold, tradedAssetIds, logWithdraw, removeReserve, instantSellCompleted | unset |
tradeitScreenshotQueue | tradeit-backend | ${TI_SCREENSHOT_SKIN_REDIS_URI}:6379 | download | unset |
tradebotQueue:${i} (per-bot) | tradeit-tradebot-server | ${TRADEIT_REDIS_URI}:6379 | deleteContainerItem, checkRevertTrade, inspect | unset |
Init guard (mainQueue + screenshotQueue): if (process.env.TI_ENV !== 'production' && Number(process.env.ENABLE_QUEUE) !== 1) return — production always enabled; non-prod is opt-in via ENABLE_QUEUE=1.
⚠️ No retention configured. Neither queue sets removeOnComplete, removeOnFail, attempts, or backoff. Failed jobs accumulate and require manual cleanup. Bull keyspace memory growth is monitored only via the ElastiCache memory alarm.
9.2 Cron handoffs that mutate Redis
| Cron | Schedule | Redis effect | File |
| Insight invalidation sweep | 0 1 * * * (01:00 UTC) | DEL all insight_* keys not refreshed by current cron | server/redis/insightRedis.js |
| Price sync | frequent | Acquires syncItemPricesRunning lock; releases on completion | server/controllers/cron.js:822-834 |
| Revert-trades sync | per-game | Reads/writes revert_trades_sync_${gameId}_lastId | server/controllers/cron.js:1529-1587 |
| Inventory sync | per-game | Reads/writes inventory_sync_${gameId}_lastScanId | server/controllers/cron.js:1754-1786 |
| Stats-manager config refresh | every 5 min | Overwrites configuration:* (no TTL set) | tradeit-stats-manager/src/admin-config/admin-config.service.ts |
9.3 Pub/sub channels (the coordination spine)
| Channel | Publisher | Payload | Subscribers |
tradestate | server/redis/tradeRedis.js:51 + tradebot-server | JSON {steamId, token, state, msg, extra?} | tradeit-socket-server src/socket.ts:61 → Socket.IO tradestate event scoped to steamId room |
bot ${botId} | server/redis/tradeRedis.js:117, 121; checkSteamGuardRedis.js:21 | Text protocol — withdraw <botId> <steamId>_<tradeHash> tradeData:<json> / sale ... / checkSteamGuard ... | bot daemon worker subscribed to its own bot-id channel |
itemReserved | server/redis/reserveAssetRedis.js:12; server/queue/mainQueue.js:142 | empty string | tradeit-socket-server src/socket.ts:65 → Socket.IO reserve-items (throttled ~1/sec) |
image | server/controllers/internal.js:154 | JSON image metadata | internal listener (image-processing worker) |
canceledtrades | tradebot-server src/index.ts:1622 | steamId | tradeit-socket-server src/socket.ts:57 |
finishedtrades | tradebot-server src/index.ts:1204 | trade key | unconfirmed live consumer |
steamDown | tradebot-server src/index.ts:3239 | timestamp | unconfirmed live consumer |
10. Configuration flags & guards
| Flag | Where | Purpose |
TRADEIT_REDIS_URI | env (all repos) | Primary Redis host |
TI_SCREENSHOT_SKIN_REDIS_URI | env (tradeit-backend) | Separate Redis for screenshot Bull queue |
TI_ENV | env (all repos) | production enables queue init unconditionally |
ENABLE_QUEUE | env (tradeit-backend) | Non-prod opt-in (1 = enable) for Bull queues |
tradeDisabled | configurations.tradeDisabled (Redis-cached) | Global trade kill switch on every trade attempt |
wikiAiContentTtlInDay | configurations row | Configurable TTL for insight_ai_content:* (default 365d) |
numBots | configurations.numBots | Bot fleet size — drives MGET length for invsizeAll730:*, stayingalive:*, containers:* |
| (no flag) | — | There is no graceful-degradation flag to run tradeit-backend without Redis. attachRedis fails the request with HTTP 500. |
11. Failure modes & runbook
| Failure | Detection | Blast radius | Recovery |
| ElastiCache primary failover | dd-trace redis.call errors spike; attachRedis returns HTTP 500 | All HTTP requests fail until reconnect | Auto-reconnect via redis v4 default; PM2 process does not auto-restart |
| Stale config after admin edit | Frontend continues showing old value up to 180s (per-key) or 360s worst-case | Trade-flag toggles, anomaly limits, pricing flags | Manual: edit again, or redis.del('configuration:<key>') |
| Bull queue backlog | bull:tradeitQueue:wait length growing | logSold, logWithdraw delayed → MySQL audit lag | Add workers; inspect failed jobs (no auto-removal) |
lockTrade:* orphan | Lock stuck after worker crash | Asset unreversible from cinv/sinv for up to 900s | Wait for TTL, or DEL lock manually |
reserved-assets_* list overflow | LLEN grows unbounded if removeReserve jobs fail | Inventory parser pre-swap hydration slows; data-node CPU climbs | OS-field redesign replaces this; transitional fix is manual LREM/LTRIM |
KEYS command leak | Hard rule violation | Site-wide latency | GUARANTEED ABSENT — verified via rg, see §13 |
| Connection storm | Many PM2 + Bull instances reconnecting after a transient failure | Redis client connection limit hit | Stagger PM2 restarts; consider connection pooling for Bull |
connect-redis session drift | Session-store connection desyncs | Users get logged out / sessions orphaned | Restart backend; sessions are 30d |
12. Observability
- Datadog APM —
dd-trace v5.84.0 auto-instruments redis calls (server/lib/tracer.js, imported first in server/index.js). All GET/SET/PUBLISH show as redis.command spans on the request waterfall.
- Pino logger —
redis.js:30 logs every connect/error event with the host context, tagged module: 'redis'. Search Datadog logs: service:tradeit-backend module:redis.
- No custom metrics — no Prometheus exporter or Datadog statsd metric for hit/miss ratios. Hit-rate is inferred from request-trace cache spans.
- ElastiCache CloudWatch — provider-level
EngineCPUUtilization, DatabaseMemoryUsagePercentage, CurrConnections, Evictions. No alarms tracked in tradeit-infra.
- Bull dashboard — no Bull Arena/UI deployed; queue inspection requires
bull CLI or ad-hoc Redis inspection.
⚠️ Observability gap: no automated monitoring of pub/sub channel publish rate, Bull failed-job count, or per-namespace TTL/eviction. Phase-2 follow-up: wire into Datadog dashboards.
13. Code map (file:line)
13.1 Connection lifecycle
| Concern | Path |
| Singleton client + middleware | tradeit-backend/server/redis.js:1-64 |
| Session-store connection bootstrap | tradeit-backend/server/index.js:73-90 |
| Subscriber connection (socket-server) | tradeit-socket-server/src/socket.ts:55-66 |
13.2 Helpers (tradeit-backend/server/redis/)
| Helper | Owns namespace |
botAccountRedis.js:4-12 | bot_acc_info:* |
categoryRedis.js:5-13 | skins_count:* |
checkSteamGuardRedis.js:1-50 (publish at L21) | steamGuard*; channel bot <botId> |
inventoryRedis.js:1-50 | cinv:*, sinv:*, globalItemInfo, hardInventoryRefresh:* |
reserveAssetRedis.js:5-39 (publish L12) | reserved-assets_*, reserved-assets_ttl_*; channel itemReserved |
tradeRedis.js:1-191 (publish L51, L117, L121) | lockTrade:*, userOpenTrades:*, userTradeLock:*, tradeDisabled; channels tradestate, bot <botId> |
seoPageContentRedis.js:1-40 | seo_page_content:*, writestuff_published_uris |
seoGiftContentRedis.js:1-15 | seo_gift_content:* |
serverStatusRedis.js:1-45 | server_status, server_status_stats:* |
steamTradeVerificationRedis.js:1-30 | steam_trade_verification* |
stripeRedis.js:1-20 | stripe_customer_id:* |
insightRedis.js:1-200 | insight_* |
admin/adminBalanceRedis.js | admin_balance_cache |
admin/adminDeleteReservedItemRedis.js | admin_delete_reserved_items:* |
13.3 Service / controller / queue / repo touch-points
| File | Role |
server/service/configurationService.js:19-62 | L1+L2 cache for configuration:* |
server/service/marketplaceConfigService.js:29-107 | Marketplace settings cache |
server/service/userService.js:238-245 | steamLevel:* cache |
server/service/serverStatusService.js:85-102 | siteStatsCache, usercount |
server/service/botsService.js:166+ | MGET fan-out across bot fleet |
server/service/oauth2/oath2WebHookService.js:28-34 | OAuth2 client cache |
server/service/couponService.js:348-353 | Coupon rate-limit |
server/controllers/cron.js:822-834, 1529-1587, 1754-1786 | Cron locks + resume cursors |
server/controllers/user.js:672-682 | stripeCountryIds cache |
server/controllers/internal.js:154 | image channel publisher |
server/controllers/inventory.js:604-611 | Search rate-limit |
server/controllers/steam.js:23 | Login key DEL |
server/queue/mainQueue.js:1-194 | tradeitQueue definition |
server/queue/screenshotQueue.js:1-101 | tradeitScreenshotQueue definition |
server/repository/containerItemsRepo.js | caskets730:* reads |
13.4 Cross-repo
| Repo | Path | Surface |
| tradeit-socket-server | src/socket.ts:57, 61, 65 | Subscribers for canceledtrades, tradestate, itemReserved |
| tradeit-tradebot-server | src/index.ts:218, 257-258, 308, 340, 367, 505, 585, 838+, 1204, 1622, 2559, 2893-2898, 3239, 3849 | All bot-state KV + pub/sub publishers |
| tradeit-admin-backend | src/services/configurationService.ts:50-71 | configuration:* writes (no DEL on admin edit) |
| tradeit-stats-manager | src/admin-config/admin-config.service.ts:20-25 | configuration:* overwrites every 5 min, no TTL |
| tradeit-stock-manager | src/app.service.ts:17, src/common/redis/redis.decorator.ts:8 | updateItemsStocks lock |
| tradeit-buff-scrapper | src/common/redis.service.ts | Generic lock wrapper |
| tradeit-oauth2-server | src/guards/login.guard.ts:52-59 | login:* writes; sess:* via connect-redis |
| tradeit-inventory-parser | src/modules/inventoryIndex/inventory.controller.ts:200-230 | indexLaneLock:* Lua-EVAL CAS DEL |
14. Open questions & known gaps
- OPEN
itemsMeta writer — read by inventory-parser at src/modules/inventoryIndex/inventory.controller.ts:112, 477 but the writer was not located. Confirm whether legacy tradeit-scripts-java or tradeit-backend writes it.
- OPEN
usercount writer — serverStatusService.js reads usercount but no SET/SETEX was found. Likely external (steam-pricing-scraper or stats-manager).
- OPEN
finishedtrades / steamDown consumers — published by tradebot-server but no subscriber found in tradeit-socket-server or tradeit-backend. Possibly legacy.
- OPEN Inventory compression scheme —
sinv:${botIndex}:compressed and cinv:*:compressed use compression; the scheme (zstd? gzip?) not located. Decompress cost not currently observed.
- OPEN Bull job retention — no
removeOnComplete set. Need ticket: removeOnComplete: { age: 24h, count: 10000 }, removeOnFail: { age: 7d }.
- OPEN stats-manager TTL gap —
configuration:* overwrites every 5 min with no TTL. If stats-manager dies mid-cycle, keys persist indefinitely with the last value. Cross-check with backend updateValueByKey DEL semantics.
- OPEN No graceful Redis-down mode —
attachRedis returns HTTP 500. Consider read-only fallbacks (serve stale local state, queue writes).
- OPEN No Datadog dashboard for Redis. Hit-rate per namespace, pub/sub publish QPS, Bull throughput, eviction count, connection count.
- OPEN ElastiCache IaC — provisioning is not in
tradeit-infra. Either import existing cluster into Terraform or document the manual setup.
- OPEN Reserved-asset list migration —
reserved-assets_* and reserved-assets_ttl_* lists are slated for removal once the OS-field design ships. Track key cleanup as a step in that rollout.
Related deep-dives: Platform Configuration · OpenSearch Patterns · Trade Lifecycle