Implement an asyncio-based chat server
DiscordSoftware Engineer · OnsiteLast updated
This question tests practical knowledge of Python's asyncio library and concurrent programming by requiring the implementation of a TCP chat server. It evaluates a software engineer's ability to manage multiple simultaneous connections, handle resource cleanup, and apply event-driven concurrency patterns — core competencies commonly assessed in backend and systems-focused interviews.
Async Chat Server (Python asyncio)
Build a TCP chat server using Python's asyncio. The server listens on a given port and serves many concurrent clients over a simple line-based text protocol. This is a phone-screen problem: the interviewer cares about correct concurrency, clean resource handling, and clear communication far more than features.
You will build the server in two parts — start with a single global room (Part 1), then add room and command support (Part 2).
Constraints & Assumptions
- Up to ~1,000 concurrent connections.
- Standard library only (
asyncio,socket, etc.) — no third-party packages. - Line-based UTF-8 text protocol; one logical message per
\n-terminated line. - No persistence required (in-memory state is fine; state may be lost on restart).
- Correctness and clear concurrency handling are valued over advanced features (no auth, no TLS, no message history needed unless asked).
Clarifying Questions to Ask
- What framing should broadcast messages use — exactly
<username>: <message>, and should server notices (joins, leaves, command output) be visually distinguishable from chat? - Are usernames required to be unique? What should happen if a chosen name is already taken?
- How should the server behave when a slow client can't keep up — drop the message, buffer it, or disconnect the client?
- Should an unknown command (e.g.
/foo) be an error, or treated as a normal chat message? - Is there a maximum line length we should enforce to bound memory?
- Should
/quitsend a goodbye, and should peers be notified when someone disconnects?
Part 1 — Single global room
Implement a server where every connected client shares one global room.
Protocol (line-based text):
- On connect, the server prompts for a username (the client's first line is taken as the username).
- After login, each subsequent line is a chat message.
- The server broadcasts each message to all other connected clients in the format
<username>: <message>.
Behavior requirements:
- A client must not receive its own message echoed back.
- Disconnects must be handled gracefully — no crashes, and the client's resources are cleaned up.
- One misbehaving client (e.g. disconnecting mid-line, sending malformed bytes) must not take down the server or affect other clients.
What This Part Should Cover
- Correct use of the asyncio streams API (
start_server,StreamReader/StreamWriter,readline,drain) with one task per client. - A shared client registry and a broadcast that excludes the sender, with awareness of why locking is/isn't needed in single-threaded asyncio.
- Robust lifecycle handling: graceful EOF/exception handling and guaranteed cleanup in a
finallyblock so a single bad client can't crash the server.
Part 2 — Rooms and chat commands
Extend the protocol so a line beginning with / is interpreted as a command; all other lines remain chat messages.
/join <room>— join (creating it if needed) a named room. Each user is in exactly one room at a time; joining a new room leaves the previous one. After joining, broadcasts go only to users in the same room./who— reply (to the requester only) with the list of usernames currently in the user's room./quit— disconnect the client cleanly.
Clarifying Questions for this Part
- Should there be a default room a user lands in before their first
/join, or are they "lobby-less" until they join? - When a user switches rooms, should the old room and the new room receive join/leave notices?
What This Part Should Cover
- A room data model that keeps the per-client → room mapping and the room → members mapping consistent on join/leave/disconnect (no stale membership, empty rooms cleaned up).
- A clean command parser that distinguishes commands from messages, scopes broadcasts to a single room, and routes
/who/errors back to only the requester. - Correct handling of
/quitand disconnects so a leaving user is removed from exactly one room's membership.
Hints
Hint 1
Where to start: Reach for the high-level streams API: `asyncio.start_server(handle_client, host, port)` gives you a `(reader, writer)` pair per connection and runs your coroutine as one independent task per client. Use `await reader.readline()` to consume one line at a time.
Hint 2
Shared state and broadcast: Keep a shared registry of connected clients (e.g. a `dict` mapping a client object/username to its `StreamWriter`). To broadcast, iterate the registry and `writer.write(...)` to everyone except the sender, then `await writer.drain()`. Because asyncio is single-threaded with cooperative scheduling, you don't need locks around the dict as long as you never `await` mid-mutation — but you must add/remove entries carefully.
Hint 3
Cleanup that always runs: Wrap the per-client read loop in `try/except/finally`. `reader.readline()` returns an empty `bytes` (`b""`) on EOF and may raise `ConnectionResetError` / `asyncio.IncompleteReadError`. Do registry removal and `writer.close()` in the `finally` so cleanup happens on every exit path.
Hint 4
Modeling rooms: Replace the single global registry with a room index: `rooms: dict[str, set[Client]]`, plus a back-reference so each client knows its current room. `/join` becomes: remove the client from its old room's set, add to the new room's set, update the back-reference. Garbage-collect a room when its set becomes empty to avoid an unbounded `rooms` dict.
Hint 5
Command dispatch: Parse only the first token: split the line once (`line.split(maxsplit=1)`) and branch on the leading `/command`. Keep broadcast scoped to `rooms[client.room]` rather than all clients. `/who` and command errors should write back only to the requesting `writer`, not broadcast.
Follow-up questions
- How would you make this horizontally scalable across multiple server processes/machines so users in the same room but on different servers still see each other (e.g. a pub/sub backbone)?
- How would you handle a slow or stuck client whose socket buffer is full without blocking everyone else — what backpressure or timeout strategy would you use?
- How would you add graceful shutdown (drain in-flight writes, notify clients) on `SIGTERM`?
- How would you bound memory and prevent abuse (max line length, max rooms/users, rate limiting)?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.