Project 25: TCP Device Telemetry Gateway

Build a single-node gateway that accepts thousands of simulated sensors, reconstructs a framed binary protocol across arbitrary TCP reads, transfers socket ownership safely, and protects the BEAM from malformed or slow clients.

Quick Reference

Attribute Value
Difficulty Level 3
Suggested Seniority Senior Elixir developer
Time Estimate 20-30 hours
Main Programming Language Elixir with Erlang networking APIs
Alternative Programming Languages Erlang, Gleam
Coolness Level Level 4
Business Potential Level 4
Prerequisites BEAM processes and supervision, binary pattern matching, TCP fundamentals
Key Topics :gen_tcp, framing, partial reads, active-once sockets, controlling processes, iodata, mailbox pressure

1. Learning Objectives

By completing this project, you will be able to:

  1. Explain why TCP delivers an ordered byte stream rather than application messages.
  2. Design and parse a length-delimited binary telemetry protocol with version, checksum, and hard limits.
  3. Handle split and coalesced frames by maintaining a per-connection receive buffer.
  4. Transfer socket ownership from an acceptor to a supervised connection process without losing messages.
  5. Compare passive, active, and active-once socket modes and apply flow control to mailbox ingestion.
  6. Disconnect idle, malformed, oversized, and persistently slow clients while keeping unrelated sessions healthy.

2. All Theory Needed (Per-Concept Breakdown)

TCP Streams, Binary Framing, and Socket Ownership

Fundamentals

TCP provides a reliable ordered stream of bytes between endpoints. It does not preserve the boundaries of calls made by a sender. One telemetry frame may arrive in several socket messages, or several frames may arrive together. The receiver therefore needs an application framing rule and a persistent buffer. A typical frame begins with a small fixed header containing magic bytes, protocol version, message type, payload length, sequence number, and checksum, followed by exactly the declared payload. Length and checksum fields are untrusted and must be bounded before allocation or deeper parsing. On the BEAM, a socket has a controlling process that receives active-mode messages and performs ownership-sensitive operations. A supervised process-per-connection design isolates parsing state and failure, but the acceptor must transfer ownership in the correct order.

Deep Dive into the concept

Imagine a device sends two frames using two send operations. TCP may deliver the first header without its payload, the rest of the first frame plus half the second, or both complete frames in one read. Network segmentation, buffering, TLS, proxies, and scheduler timing all affect delivery chunks. Code that pattern matches one socket message as one frame will pass localhost demos and fail in production. The invariant is that the parser consumes zero or more complete frames from a buffer and retains the exact incomplete suffix for the next read.

Define a protocol that makes safe incremental parsing possible. For this project, use a fixed header with two magic bytes, one version byte, one type byte, an unsigned payload length, an unsigned sequence number, and a checksum over the header fields and payload according to a documented rule. Choose explicit network byte order. Set a maximum payload such as 4 KiB and a maximum buffered unparsed bytes slightly above the largest frame. Reject unsupported versions and unknown types according to policy; do not trust length enough to wait indefinitely for gigabytes.

Incremental parsing has three outcomes. If fewer bytes than the fixed header are present, request more. If the header is complete, validate magic, version, type, and length before examining payload. If the buffer does not yet contain the full declared frame, retain it and request more. If it does, split one frame from the remainder, validate checksum and semantic fields, emit the decoded event, and loop over the remainder. The loop must bound frames handled per turn so one noisy connection does not monopolize its process scheduler slice.

Binary pattern matching expresses the header clearly, but the pattern is not the whole security boundary. Guard allowable lengths before extracting a payload of attacker-declared size. Avoid repeatedly concatenating growing binaries because that can copy data and retain large parent binaries. Keep buffers bounded, process frames promptly, and consider copying a small retained suffix when it would otherwise reference a huge received binary. Build acknowledgements as iodata so headers, sequence fields, and payload fragments can be sent without manually flattening everything.

The socket owner matters. :gen_tcp.accept returns a socket controlled by the accepting process. If a new connection process is started, the acceptor must transfer control with :gen_tcp.controlling_process. Active messages sent before transfer may land in the acceptor’s mailbox. A safe handshake starts the connection process in a waiting state, transfers ownership, then signals it to configure active-once reception. On failure, close the socket and terminate the child cleanly. Multiple acceptors may share a listening socket according to the selected acceptor design, but each accepted socket must have exactly one clear owner.

Passive mode makes explicit recv calls. It naturally bounds mailbox growth but can block the connection process awaiting bytes; timeouts and shutdown handling are required. Active mode sends socket data as process messages continuously. It is convenient but lets a fast client outpace parsing and fill the mailbox. Active-once sends at most one data message and automatically returns to passive; the owner reenables it only after processing current input. This creates a simple per-connection demand gate. Reenable after updating the buffer and applying downstream admission policy, not immediately on receipt.

Active-once does not solve every overload problem. If decoded telemetry is synchronously sent to a single slow aggregator, connection processes may block. If it is asynchronously cast without bounds, the aggregator mailbox grows. The gateway needs an admission boundary: a bounded local batch, a fast non-blocking sink with queue limits, or a policy that pauses socket rearming until capacity recovers. Define what happens when capacity is exhausted—delay acknowledgement, reject a frame with a busy status, sample noncritical readings, or close persistently abusive connections. Silent unbounded buffering is not a policy.

Per-connection state includes device authentication status, receive buffer, last accepted sequence, last activity monotonic time, checksum/error counters, and acknowledgement window. Require a HELLO frame before telemetry. Bind the device ID to the connection after validation and reject identity changes. Sequence numbers detect duplicates and gaps but are not a cryptographic replay defense unless combined with stronger authentication and durable state. For this lab, report gap/duplicate metrics and acknowledge the last accepted sequence.

Timeouts need independent meanings. A handshake deadline closes clients that connect and send nothing. An idle timeout closes established devices that stop sending and fail heartbeat policy. A frame assembly timeout closes a client that declares a frame length and trickles bytes indefinitely. Send timeouts protect the process from a peer that does not read acknowledgements. Use monotonic time for durations. Avoid one global timer process becoming a bottleneck; process timers or scheduled checks can enforce local deadlines.

Supervision should reflect ownership. A listener supervisor owns acceptors and a dynamic supervisor owns connection workers. Normal client disconnect is expected, not an error requiring noisy restart. A parser crash should terminate only that connection and close its socket. Restart intensity must not turn a burst of malformed clients into collapse of the entire listener subtree. Telemetry should expose active connections, accepts/rejects, bytes, complete frames, checksum errors, frame gaps, buffer high-water marks, mailbox lengths, and disconnect reasons with bounded labels.

Test the stream property directly. Build valid frame bytes, then deliver every possible two-way split, randomized multi-splits, many concatenated frames, garbage prefixes, and invalid lengths. The same logical byte stream must produce the same decoded frames regardless of chunk boundaries. Integration tests use real loopback sockets to prove ownership transfer and active-once behavior. Load tests simulate slow readers, partial senders, burst senders, and abrupt closes while measuring mailbox and memory bounds.

This project is intentionally single-node. It stops after validated frames enter a local bounded sink. It does not federate sites, replicate streams, or create a distributed event bus. That scope keeps attention on transport truth: byte framing, socket lifecycle, ownership, and per-connection flow control.

How this fits on projects

Each socket becomes one isolated connection process. Binary patterns implement incremental framing, active-once controls ingress, and supervision keeps acceptors and sessions recoverable without confusing normal disconnects with infrastructure failure.

Definitions & key terms

  • Byte stream: Ordered bytes with no application message boundaries.
  • Frame: Application-defined message encoded within the stream.
  • Controlling process: BEAM process authorized to receive active socket messages and transfer ownership.
  • Active-once: Socket mode that delivers one message then becomes passive.
  • Iodata: Nested binaries/byte lists accepted efficiently by many I/O APIs.
  • Slowloris behavior: Holding resources by sending valid-looking data extremely slowly.
  • Receive buffer: Unconsumed bytes retained between socket deliveries.

Mental model diagram

                +---------------- Listener Supervisor ----------------+
devices ---> [Listen Socket] ---> [Acceptor Pool]                      |
                                      | accept socket                  |
                                      | start child + transfer owner   |
                                      v                                |
                           [Dynamic Connection Supervisor] <-----------+
                                      |
                       +--------------+---------------+
                       v                              v
              [Connection Process]           [Connection Process]
              active-once socket              active-once socket
              buffer + parser                 buffer + parser
                       | validated frames
                       v
                 [Bounded Local Sink] -> acknowledgements/metrics

How it works (step-by-step, with invariants and failure modes)

  1. Accept a socket under a listener-owned acceptor.
  2. Start a waiting connection child and transfer socket control.
  3. Signal readiness; child sets options and arms active-once.
  4. Append delivered bytes to the bounded receive buffer.
  5. Parse as many complete frames as the per-turn budget allows.
  6. Validate checksum, handshake, sequence, and payload semantics.
  7. Admit events to the bounded sink and send acknowledgement iodata.
  8. Rearm active-once only when ready for another chunk.
  9. Invariants: one owner per socket; unparsed buffer remains bounded; only complete validated frames reach the sink.
  10. Failure modes: transfer race, oversized declared length, slow frame assembly, sink overload, mailbox growth, abrupt close, and checksum storms.

Minimal concrete example

PROTOCOL TRANSCRIPT — not runnable code

HEADER:
  magic:       2 bytes = "TG"
  version:     1 byte
  type:        1 byte (HELLO=1, TELEMETRY=2, HEARTBEAT=3, ACK=128, ERROR=255)
  payload_len: 2 bytes unsigned, network order, maximum 4096
  sequence:    4 bytes unsigned, network order
  checksum:    4 bytes over version..payload

PARSER OUTCOMES:
  fewer than header bytes -> NEED_MORE(buffer)
  invalid header/length   -> CLOSE(protocol_violation)
  incomplete payload      -> NEED_MORE(buffer)
  complete valid frame    -> EMIT(frame, remainder), then parse remainder

Common misconceptions

  • “One send equals one receive.” TCP preserves order, not send-call boundaries.
  • “Active-once provides end-to-end backpressure.” It bounds socket messages but downstream queues still need policy.
  • “The new process owns the accepted socket automatically.” Ownership remains with the acceptor until transferred.
  • “Checksums authenticate devices.” They detect corruption, not malicious forgery.

Check-your-understanding questions

  1. Why must the parser retain a suffix between reads?
  2. What race occurs if active reception begins before ownership transfer?
  3. When should the connection process rearm active-once?

Check-your-understanding answers

  1. A frame can be split at any byte boundary, including inside its header.
  2. Data messages can arrive in the acceptor mailbox instead of the connection worker.
  3. After processing current bytes and confirming downstream capacity/policy.

Real-world applications

  • IoT and industrial telemetry ingestion.
  • Custom financial, logistics, and multiplayer TCP protocols.
  • Gateways translating device streams into validated internal events.

Where you’ll apply it

  • Protocol and session requirements in Section 3.
  • listener/connection ownership in Sections 4–5.
  • arbitrary-chunk, slow-client, and load tests in Section 6.

References

  • OTP gen_tcp: https://www.erlang.org/doc/apps/kernel/gen_tcp.html
  • Elixir patterns and guards: https://hexdocs.pm/elixir/patterns-and-guards.html
  • DynamicSupervisor: https://hexdocs.pm/elixir/DynamicSupervisor.html
  • Erlang efficiency guide: https://www.erlang.org/doc/system/eff_guide.html

Key insight

TCP correctness begins when you stop treating receives as messages and instead make framing, ownership, and admission explicit.

Summary

Maintain a bounded incremental parser per connection, validate untrusted lengths before extraction, transfer each socket to one supervised owner, and use active-once plus downstream admission policy to prevent fast peers from turning into mailbox failures.

Homework/Exercises to practice the concept

  1. Split one frame at every byte boundary and predict parser state after the first chunk.
  2. Trace the acceptor/child handshake if transfer succeeds, child start fails, or the peer closes early.
  3. Choose disconnect policies for handshake timeout, checksum burst, sink overload, and idle device.

Solutions to the homework/exercises

  1. Every split before the full frame yields NEED_MORE; the exact prefix remains buffered, and completion emits once.
  2. Only signal readiness after successful transfer; close on any failed handoff; early close becomes a normal connection termination.
  3. Use short handshake deadline, bounded error threshold, pause then close persistent overload, and documented heartbeat/idle timeout.

3. Project Specification

3.1 What You Will Build

Build a listener, acceptor pool, dynamically supervised connection processes, binary protocol parser/encoder, device session rules, bounded telemetry sink, metrics reporter, and simulator capable of valid, fragmented, bursty, slow, and malformed clients.

3.2 Functional Requirements

  1. Accept many loopback TCP device connections through a supervised acceptor design.
  2. Require a valid HELLO before telemetry.
  3. Reconstruct frames across arbitrary split/coalesced deliveries.
  4. Reject unsupported versions, oversized payloads, invalid checksums, and protocol-order errors.
  5. Track sequence duplicates/gaps and acknowledge accepted frames.
  6. Enforce handshake, frame-assembly, idle, and send deadlines.
  7. Bound receive buffers, per-turn parsing, mailbox ingestion, and local sink capacity.
  8. Report disconnect reason and session metrics.

3.3 Non-Functional Requirements

  • Demonstrate 5,000 simulated idle/light connections on the target development machine or document the adjusted capacity baseline.
  • Under burst load, connection mailbox and receive-buffer high-water marks remain within declared limits.
  • One malformed client cannot terminate listener, acceptors, sink, or unrelated sessions.
  • The protocol has a versioning and compatibility policy.

3.4 Example Usage / Output

$ telemetry-sim connect --devices 1000 --rate 2/s --fragment random
connected=1000 hello_ok=1000 frames_sent=120000 acks=120000
checksum_errors=0 sequence_gaps=0 disconnects=0

$ gatewayctl sessions --top-buffer 3
device=sensor-0441 buffer=37 mailbox=0 last_seq=881 state=established
device=sensor-0088 buffer=16 mailbox=1 last_seq=902 state=established

3.5 Data Formats / Schemas / Protocols

Telemetry payload contains device monotonic sample time, signed temperature integer in hundredths, battery millivolts, and status flags. HELLO includes bounded device ID and authentication fixture token. ACK echoes the last accepted sequence and status.

3.6 Edge Cases

  • Header split after every possible byte.
  • Three frames plus half a fourth delivered together.
  • Maximum-length and one-byte-too-large payload.
  • Client sends telemetry before HELLO or changes device identity.
  • Sequence wrap, duplicate, and gap.
  • Declared frame trickled one byte per timeout interval.
  • Peer never reads acknowledgements.
  • Sink overload while socket data continues.

3.7 Real World Outcome

3.7.1 How to Run (Copy/Paste)

$ mix test
$ mix run priv/demo_gateway.exs
$ telemetry-sim run priv/scenarios/mixed_clients.json
$ gatewayctl metrics

3.7.2 Golden Path Demo (Deterministic)

The mixed scenario starts 200 normal devices, one random-fragment device, one burst client, one slowloris client, and three malformed clients. Valid frames reach the sink exactly once; bad sessions close with expected reasons; healthy sessions remain.

3.7.3 Exact terminal transcript

$ mix run priv/demo_gateway.exs
listener port=4545 acceptors=8 active_mode=once max_frame=4110
clients connected=206 hello_ok=203
valid_frames=20000 sink_accepted=20000 acknowledgements=20000
closed checksum_limit=1 oversized_frame=1 protocol_order=1 frame_timeout=1
healthy_remaining=202 parser_mismatches=0
max_connection_buffer=4109 max_connection_mailbox=2 listener_restarts=0
DEMO PASS

4. Solution Architecture

4.1 High-Level Design

                              [Gateway Supervisor]
                         /             |              \
                [Listener]      [Connection DynSup]   [Bounded Sink]
                    |
              [Acceptor Pool]
                    | accepted socket (acceptor owns)
                    v
        start waiting child -> transfer control -> ready
                                      |
                                      v
                            [Connection Process]
                            socket + buffer + state
                              |              ^
                      validated frame     ACK iodata
                              v
                         [Bounded Sink]

4.2 Key Components

Component Responsibility Key Decision
Listener Open configured socket Explicit packet raw/binary options
Acceptor Pool Accept and hand off sessions Waiting-child ownership handshake
Connection Worker Own socket and parse stream Active-once with bounded buffer
Protocol Codec Incremental decode and ACK encode Closed parser outcomes
Session State Enforce HELLO/sequence/timeouts Monotonic deadlines
Bounded Sink Admit validated telemetry Explicit overload behavior
Simulator Generate transport adversaries Deterministic seeds/scenarios

4.3 Data Structures (No Full Code)

  • Parser state: buffered binary and protocol version.
  • Session state: socket, device identity, phase, last sequence/activity, error counters, buffer high-water mark.
  • Telemetry event: device ID, sequence, sample time, temperature integer, battery, flags, receive time.
  • Disconnect reason: normal close or closed taxonomy of timeout/protocol/overload errors.

4.4 Algorithm Overview

Parsing is O(total received bytes); each byte should be inspected a bounded number of times. Per-frame semantic work is O(payload size). Session lookup is local process state, avoiding a central routing bottleneck for frame parsing.


5. Implementation Guide

5.1 Development Environment Setup

Use an actively supported Elixir/OTP pair and OTP :gen_tcp. Add telemetry/metrics tooling only after parser and lifecycle tests pass. Raise local file-descriptor limits deliberately for large connection tests and record the baseline rather than changing system configuration silently.

5.2 Project Structure

telemetry_gateway/
├── lib/telemetry_gateway/{application,listener,acceptor,connection_supervisor,connection}.ex
├── lib/telemetry_gateway/protocol/{frame,decoder,encoder}.ex
├── lib/telemetry_gateway/{sink,metrics}.ex
├── lib/telemetry_sim/{client,scenario}.ex
├── priv/scenarios/
└── test/{protocol,ownership,integration,load}/

5.3 The Core Question You’re Answering

“How can a BEAM process turn an arbitrary TCP byte stream into trustworthy bounded messages without losing socket lifecycle control?”

5.4 Concepts You Must Understand First

  1. TCP streams — TCP/IP Illustrated, Volume 1, 2nd Edition, Chapters 12–17.
  2. Elixir binaries — Programming Elixir 1.6, Chapter 11, “Strings and Binaries”.
  3. BEAM processes — Elixir in Action, 3rd Edition, Chapters 5–6.
  4. Supervision — Designing for Scalability with Erlang/OTP, supervision chapters.

5.5 Questions to Guide Your Design

  1. What maximum bytes can one untrusted connection retain?
  2. When exactly is the socket owner transferred and reception armed?
  3. Which downstream condition delays active-once rearming?
  4. Which disconnects are normal versus faults worth alerting?
  5. How will protocol evolution remain backward compatible?

5.6 Thinking Exercise

Draw a timeline where the peer sends data immediately after connect, the child starts slowly, the transfer succeeds, and the peer closes. Identify which process may receive each message and how the handshake prevents mailbox loss.

5.7 The Interview Questions They’ll Ask

  1. Why can TCP split or combine application writes?
  2. Compare passive, active, and active-once socket modes.
  3. What is a controlling process, and how do you hand off safely?
  4. How would you defend against an attacker declaring a huge payload length?
  5. Why can active-once still lead to downstream overload?
  6. How do iodata and binary subterms affect memory behavior?

5.8 Hints in Layers

Hint 1: Parser before sockets — Prove one pure incremental decoder against arbitrary chunk boundaries.

Hint 2: One connection lifecycle — Start a waiting process, transfer ownership, then arm reception.

Hint 3: Make slowness visible — Track buffer, mailbox, last activity, and sink admission separately.

Hint 4: Attack transport assumptions — Randomize chunks and send timing while keeping logical frames fixed.

5.9 Books That Will Help

Topic Book Chapter
TCP semantics TCP/IP Illustrated, Vol. 1, 2nd Edition Ch. 12–17
Binaries Programming Elixir 1.6 Ch. 11
Process architecture Elixir in Action, 3rd Edition Ch. 5–6
Supervision Designing for Scalability with Erlang/OTP Supervision chapters

5.10 Implementation Phases

Phase 1: Protocol and Pure Parser (5-7 hours)

  • Specify frames, build encoder/decoder pseudocode, exhaustive split tests, and semantic validation.

Phase 2: Socket Ownership and Sessions (7-11 hours)

  • Build listener, acceptors, dynamic connection supervision, active-once loop, timeouts, and ACKs.

Phase 3: Capacity and Failure Evidence (8-12 hours)

  • Add bounded sink, simulator, overload policy, metrics, load/fault scenarios, and runbook.

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
Framing Delimiter, fixed length, length header Fixed header + bounded length Binary payload safety and evolution
Socket mode Passive, active, active-once Active-once Explicit per-message admission
Session model Central server, process per connection Process per connection Isolation and local parser state
Handoff Child reads immediately, readiness handshake Readiness handshake Prevent owner race
Overload Unbounded queue, pause/reject/close policy Bounded explicit policy Protect BEAM memory

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Parser property Chunk boundaries do not change meaning every split/random partitions
Protocol rejection Bound untrusted fields length, version, checksum
Ownership integration Correct controlling process immediate-send handoff
Lifecycle Timeouts and closes idle, slow frame, peer reset
Overload Bound mailboxes and sink burst and slow consumer
Capacity Measure target concurrency thousands of simulated devices

6.2 Critical Test Cases

  1. Every two-way split of a valid frame emits the same one event.
  2. Random chunks of 100 concatenated frames emit all frames in order.
  3. Oversized length is rejected before payload wait/allocation.
  4. Immediate peer data after accept reaches the connection owner, not the acceptor mailbox.
  5. Active-once never produces more than one outstanding socket data message per connection under the controlled scenario.
  6. Slow frame assembly and non-reading peer meet their respective deadlines.
  7. Sink overload activates documented pause/reject/close behavior without unbounded mailbox growth.
  8. Malformed-client storm leaves listener and healthy sessions running.

6.3 Test Data

Generate valid HELLO, telemetry, heartbeat, ACK, maximum frames, bad magic/version/type/length/checksum, duplicate/gapped sequence, partial header/payload, random chunks, and one-byte trickle streams.

6.4 Definition of Done

  • Protocol specification defines byte order, versions, maximums, checksum, and session order.
  • Pure parser passes exhaustive split and randomized coalescing tests.
  • Socket handoff proves the intended controlling process receives data.
  • Active-once is rearmed only after processing and admission.
  • Handshake, frame, idle, and send deadlines are independently tested.
  • Receive buffer, mailbox, and sink remain within declared bounds under burst load.
  • Malformed sessions close without restarting listener or harming healthy clients.
  • Mixed-client demo ends with exact sink/frame agreement and DEMO PASS.

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

Problem: “Parser fails only outside localhost.”

  • Why: It assumes one receive contains one frame.
  • Fix: Keep an incremental buffer and parse complete frames in a loop.
  • Quick test: Feed every possible split and randomized chunk grouping.

Problem: “Data appears in the acceptor mailbox.”

  • Why: Socket activation occurred before controlling-process transfer.
  • Fix: Use waiting child, transfer, ready signal, then active-once.
  • Quick test: Have the peer send immediately during handoff.

Problem: “Memory grows under burst traffic.”

  • Why: Full active mode or downstream casts create unbounded mailboxes.
  • Fix: Use active-once and bounded sink admission with explicit overload policy.
  • Quick test: Slow the sink and chart mailbox/buffer high-water marks.

7.2 Debugging Strategies

  • Log connection ID, device ID after authentication, parser outcome, buffer bytes, and disconnect reason.
  • Inspect process mailbox length and socket statistics for a single troubled session.
  • Replay captured bytes through the pure parser without a socket.
  • Use deterministic simulator seeds so timing scenarios can be repeated.

7.3 Performance Traps

  • Repeatedly appending large binaries and retaining huge parent binaries for tiny suffixes.
  • Flattening iodata before every send.
  • Parsing unlimited frames in one process turn.
  • Centralizing all frame parsing or session state in one GenServer.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add protocol version negotiation in HELLO/ACK.
  • Add a packet capture formatter for decoded frame summaries.

8.2 Intermediate Extensions

  • Add TLS transport while preserving ownership and framing tests.
  • Add per-device authentication and key rotation fixtures.
  • Add property-generated frame sequences and model-based session tests.

8.3 Advanced Extensions

  • Build a multi-listener rolling protocol migration.
  • Add load shedding based on scheduler and sink health.
  • Compare a mature acceptor library with the hand-built lab and document ownership differences.

9. Real-World Connections

9.1 Industry Applications

Industrial gateways, GPS trackers, building sensors, payment terminals, and multiplayer servers often use long-lived framed TCP protocols. The process-per-connection model turns transport sessions into isolated state machines well suited to the BEAM.

  • Ranch provides production-grade acceptor and protocol process abstractions.
  • Thousand Island provides Elixir-oriented socket server infrastructure.
  • :gen_tcp remains the foundational OTP interface this project intentionally exposes.

9.3 Interview Relevance and Scope Boundary

P25 teaches one-node TCP framing, ownership, binary parsing, and connection flow control. Project 13 teaches federated cross-site event routing and WAN backpressure. P25 ends at a bounded local sink and deliberately avoids distribution, pub/sub federation, and cross-node delivery semantics.


10. Resources

10.1 Essential Reading

  • OTP gen_tcp: https://www.erlang.org/doc/apps/kernel/gen_tcp.html
  • OTP Inet options: https://www.erlang.org/doc/apps/kernel/inet.html
  • DynamicSupervisor: https://hexdocs.pm/elixir/DynamicSupervisor.html
  • Elixir patterns and guards: https://hexdocs.pm/elixir/patterns-and-guards.html
  • Elixir IO and iodata: https://hexdocs.pm/elixir/IO.html#module-io-data

10.2 Standards and Further Study

  • RFC 9293, Transmission Control Protocol: https://www.rfc-editor.org/rfc/rfc9293
  • TCP/IP Illustrated, Volume 1, 2nd Edition, Chapters 12–17.
  • Erlang efficiency guide: https://www.erlang.org/doc/system/eff_guide.html
  • Ranch documentation: https://ninenines.eu/docs/en/ranch/