Feature status: DIST-001 in the canonical register.
Backlog / proposal. The system runs on a single Node instance per deployment today.
Several key subsystems assume a single instance:
LlmServicekeeps a 5-minute response cache in an in-processMap(packages/backend/src/infrastructure/llm/llm.service.ts:289). Two instances will duplicate LLM calls and cannot share cache hits.- There is no instance heartbeat, so a failover/restart cannot tell whether another instance is currently running the orchestrator or a cron.
- While BullMQ queues are already Redis-backed and can be shared, the orchestrator heartbeat and
SessionsServicemutex are local to one process.
This limits horizontal scaling and makes rolling deployments risky.
llm.service.ts:289usesprivate cache = new Map<string, CachedLlmResponse>().sse.service.tspublishes to Redisspa:sseand subscribes; this part is already multi-instance compatible.queue.factory.tsuses BullMQ with Redis; workers in multiple instances will pick from the same queues.orchestrator.service.tswrites a heartbeat toSHARED_REDISwith keyORCHESTRATOR_HEARTBEAT_KEYand TTL, but there is no leader election / fencing token.BrowsingSessionServiceuses a staticsessionMutex(in-process Promise), which only serializes within one process.
<ref_snippet file="/Users/valentinyakovlev/projects/agents/social-poster-agent/packages/backend/src/infrastructure/llm/llm.service.ts" lines="285-295" />
- Shared response cache. Replace the in-process
Mapwith a Redis-backed cache (or hybrid: L1 in-process + L2 Redis) keyed by SHA-256 of prompt/system/temperature/maxTokens/model/role. AddLLM_CACHE_SHARED=truefeature flag. - Instance heartbeat / leader election.
- Each instance writes
spa:instance:${hostname}:${pid}with TTL. - Orchestrator only starts if it can acquire a Redis lock (Redlock or
SET NX EX) withORCHESTRATOR_LEADER_KEY.WatchdogCronchecks the leader, not just any heartbeat. - Same for expensive singletons like
BrowsingSessionServiceglobal mutex (replace static Promise with Redis distributed lock).
- Each instance writes
- Request/session affinity. If stateful browser contexts are not fully shareable, route posting/engagement jobs to the instance that owns the session, or persist
storageStateand allow any instance to re-acquire. - Metrics per instance. Add instance ID to traces, logs, and Prometheus metrics to see load distribution.
No Prisma schema changes. Redis keys:
spa:cache:llm:<sha256> -> JSON { content, usage, model, expiresAt }
spa:instance:<id> -> { startedAt, version, roles }
spa:leader:orchestrator -> <instanceId>
spa:lock:engagement -> <instanceId> // distributed browsing session mutex
infrastructure/llm/llm.service.ts— cache abstraction.infrastructure/redis/redis.module.ts— provide aRedisCachehelper.modules/orchestrator/orchestrator.service.ts— leader lock beforerunGraphLoop().modules/engagement/browsing-session.service.ts— distributed lock instead ofstatic sessionMutex.infrastructure/queue/queue.factory.ts— already shared; ensure job idempotency works across instances.
- Shared cache increases Redis traffic; keep values compressed and TTL short (5 min).
- Leader election with Redis is not bulletproof in network partitions; acceptable for non-critical orchestration, but critical posting paths should remain queue-based.
- Browser contexts are tied to a process; moving a posting job between instances may require re-login unless
storageStateis robust. - Need graceful shutdown: release locks and flush cache before exit.
M–L (2–4 weeks). Leader election and distributed engagement lock are the hard parts; shared LLM cache is relatively straightforward.
infrastructure-llm.md(cache, concurrency)infrastructure-redis.mdorchestrator.mdengagement.md