Implement an asyncio-based chat server

Read the full interview experience this question came from →

Quick Overview

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.

Implement an asyncio-based chat server

Company: Discord

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Onsite

## 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 `/quit` send 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. ```hint 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 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 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. ``` #### 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 `finally` block 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. ```hint 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 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. ``` #### 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 `/quit` and disconnects so a leaving user is removed from exactly one room's membership. ### What a Strong Answer Covers Across both parts, the interviewer is looking for: - An idiomatic, single-event-loop asyncio design (no threads, no blocking I/O on the loop) with one supervised task per connection. - Backpressure awareness: calling `await writer.drain()` so a fast sender or many recipients can't blow up memory, and a stated policy for slow consumers. - Resource-leak-free lifecycle: every connection path (normal quit, EOF, reset, exception) removes the client from all shared state and closes the transport. - Clear, testable structure: small functions (handle connection, broadcast, dispatch command) and a story for how you'd manually test it (e.g. multiple `nc`/telnet sessions). ### 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)?

Overview: 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.

Read the full Discord Software Engineer interview experience this question came from

Discord logo
Discord
Oct 25, 2025
mediumSoftware EngineerOnsiteCoding & Algorithms
73
0

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 Guidance

  • 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 /quit send 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 Guidance

  • 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 finally block 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 Guidance

  • 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 Guidance

  • 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 /quit and disconnects so a leaving user is removed from exactly one room's membership.

What a Strong Answer Covers Guidance

Across both parts, the interviewer is looking for:

  • An idiomatic, single-event-loop asyncio design (no threads, no blocking I/O on the loop) with one supervised task per connection.
  • Backpressure awareness: calling await writer.drain() so a fast sender or many recipients can't blow up memory, and a stated policy for slow consumers.
  • Resource-leak-free lifecycle: every connection path (normal quit, EOF, reset, exception) removes the client from all shared state and closes the transport.
  • Clear, testable structure: small functions (handle connection, broadcast, dispatch command) and a story for how you'd manually test it (e.g. multiple nc /telnet sessions).

Follow-up Questions Guidance

  • 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)?