At 100+ profiles, hardcoding profile_ids in workers breaks — you need a pool manager that tracks tier, health, and lease state. This recipe sits between CMDB and the queue worker: workers ask the pool for an available profile, not a static list.
Architecture
CMDB (Postgres/JSON) → source of truth: profile_id, tier, geo, ban_status Redis pool sets → mlx:pool:prod, mlx:pool:warm, mlx:pool:burn Health cron (5 min) → probe start/stop → move between sets Queue worker → acquire(profile_id) → lease → job → release
CMDB profile row
profile_id: uuid-... tier: prod | warm | burn geo: US platform: ebay.com ban_status: ok | suspect | burned last_health_at: 2026-06-17T10:00:00Z last_job_at: 2026-06-17T09:55:00Z health_fail_count: 0
Redis pool sets
import redis
r = redis.Redis(decode_responses=True)
def sync_pool(profile_id: str, tier: str, ban_status: str):
"""Call after CMDB update or health probe."""
for t in ("prod", "warm", "burn"):
r.srem(f"mlx:pool:{t}", profile_id)
if ban_status == "burned":
r.sadd("mlx:pool:burn", profile_id)
else:
r.sadd(f"mlx:pool:{tier}", profile_id)
Acquire profile (worker API)
import random
def acquire_profile(tier: str = "prod", geo: str | None = None) -> str | None:
"""Return profile_id or None if pool exhausted."""
candidates = list(r.smembers(f"mlx:pool:{tier}"))
random.shuffle(candidates)
for profile_id in candidates:
lease_key = f"mlx:lease:{profile_id}"
if r.exists(lease_key):
continue # already leased
if geo and not profile_geo_matches(profile_id, geo):
continue
if r.set(lease_key, "pool-manager", nx=True, ex=900):
return profile_id
return None
Health probe cron
async def health_probe(profile_id: str) -> bool:
"""Lightweight: API start → CDP ping → stop. No platform login."""
try:
async with httpx.AsyncClient(timeout=90) as client:
start = await client.post(
f"{MLX}/profile/start",
json={"profile_id": profile_id, "headless": True},
headers={"Authorization": TOKEN},
)
start.raise_for_status()
cdp = start.json().get("cdp_url")
ok = await cdp_ping(cdp) # websocket connect + close
await client.post(
f"{MLX}/profile/stop",
json={"profile_id": profile_id},
headers={"Authorization": TOKEN},
)
return ok
except Exception:
return False
async def run_health_cycle():
for tier in ("prod", "warm"):
for profile_id in r.smembers(f"mlx:pool:{tier}"):
if r.exists(f"mlx:lease:{profile_id}"):
continue
ok = await health_probe(profile_id)
if not ok:
fails = r.incr(f"mlx:health_fail:{profile_id}")
r.expire(f"mlx:health_fail:{profile_id}", 86400)
if fails >= 3:
sync_pool(profile_id, tier, "burned")
notify_ops(f"Profile {profile_id} moved to burn pool")
else:
r.delete(f"mlx:health_fail:{profile_id}")
Tier routing rules
| Tier | Use | Pool behavior |
|---|---|---|
prod | Live seller/ad accounts | Health probe every 5 min; strict ban tagging |
warm | New profiles warming up | Manual browse jobs only; no bulk automation |
burn | Banned or suspect profiles | Excluded from acquire; forensics only |
Ban signals from DLQ handler or observability auto-move profiles to burn.
Warm pool bootstrap
# New profiles enter warm tier for 7–14 days
def onboard_profile(profile_id: str, geo: str):
cmdb_insert(profile_id, tier="warm", geo=geo, ban_status="ok")
sync_pool(profile_id, "warm", "ok")
schedule_warm_jobs(profile_id) # manual browse, no platform login automation
Operational rules
- Never acquire from burn — replay requires human ack + new proxy
- Health probe off-peak — schedule via health check cron; respect Multilogin plan concurrent cap
- Pool depth alert — if
SCARD mlx:pool:prod < 5for tier, page ops - Geo filter — marketplace jobs must acquire matching
geo
Related
Queue worker
Health check cron
Scaling multi-account
DLQ handler
Observability
Ban recovery runbook
Debug runbook
Code hub
Disclosure: MLX-MMO affiliated with Multilogin. Verify API endpoints against official documentation.