ARTICLE AD BOX
I'm running a trading bot that connects to the Bybit exchange. Each trading strategy runs as its own process with an asyncio event loop managing three coroutines: a private WebSocket (order fills), a public WebSocket (price ticks for TP/SL), and a main polling loop that fetches candles every 10 seconds.
The old version of my bot had no WebSocket at all , just REST polling every 10 seconds. It ran perfectly fine on 0.5 vCPU / 512 MB RAM.
Once I added WebSocket support, the process gets OOM-killed on 512 MB containers and only runs stable on 1 GB RAM.
# Old code (REST polling only) — works on 512 MB VSZ: 445 MB | RSS: ~120 MB | Threads: 4 # New code (with WebSocket) — OOM killed on 512 MB VSZ: 753 MB | RSS: ~109 MB at time of kill | Threads: 8The VSZ jumped +308 MB just from adding a WebSocket library ,before any connection is even made. The kernel OOM log confirms it's dying from demand-paging as the process loads library pages into RAM at runtime.
| websocket-client | Thread-based | 9 OS threads per strategy, high VSZ |
| websockets >= 13.0 | Async | VSZ 753 MB, OOM on 512 MB |
| aiohttp >= 3.9 | Async | Same VSZ ballpark, still crashes |
All three cause the same problem. The old requirements with no WebSocket library at all stays at 445 MB VSZ.
Python 3.11, running inside Docker on Ubuntu 20.04 (KVM hypervisor) One subprocess per strategy, each with one asyncio event loop Two persistent WebSocket connections per process (Bybit private + public stream) Blocking calls (DB writes, REST orders) offloaded via run_in_executor Server spec: 1 vCPU / 1 GB RAM (minimum that works), 0.5 vCPU / 512 MB is the targetIs there a lightweight Python async WebSocket client that doesn't bloat VSZ this much?
Edited:
Here's the relevant code. The WS implementation using aiohttp:
# Two coroutines, both scheduled via asyncio.TaskGroup in the entrypoint async def run_private(self) -> None: async with aiohttp.ClientSession() as session: while True: try: async with session.ws_connect(WS_PRIVATE, heartbeat=None) as ws: await ws.send_str(json.dumps({"op": "auth", "args": [...]})) await ws.send_str(json.dumps({"op": "subscribe", "args": ["execution", "position"]})) ping_task = asyncio.create_task(self._run_ping(ws)) try: async for msg in ws: if msg.type == aiohttp.WSMsgType.TEXT: await self._on_message(msg.data) elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR): break finally: ping_task.cancel() except (aiohttp.ClientError, OSError): pass await asyncio.sleep(2) async def run_public(self) -> None: # identical structure, different URL # Entrypoint async def main(): async with asyncio.TaskGroup() as tg: tg.create_task(ws.run_private()) tg.create_task(ws.run_public()) tg.create_task(main_loop()) # fetches candles, REST polling every 10s tg.create_task(log_sink.run()) # async log flusher asyncio.run(main()) # Minimal reproduction — no connection made, just import import aiohttp # VSZ jumps ~150–200 MB from baseline import websockets # VSZ jumps ~300 MB from baseline # No sockets opened, no event loop running