Library / SDK
mtrudel/thousand_island avatar
mtrudel/thousand_island

Thousand Island hands the acceptor pool to OTP and keeps one interface

Thousand Island is a pure Elixir socket server

996 stars59 forksElixirMIT

At a glance

What is it?
Thousand Island is a pure Elixir socket server with almost no dependencies, built around a supervision tree of listeners, acceptor pools and per-connection handler processes. Your code implements one behaviour, and the library deals with accepting, draining and telemetry.
Who is it for?
Thousand Island fits Elixir teams that want a socket layer they can read end to end, that need connection draining with a predictable timeout, and that would rather own one behaviour than a framework's configuration surface.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 8 days ago.
What is it written in?
Mainly Elixir, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A socket server you host inside your own application

Thousand Island is described as a modern, pure Elixir socket server, inspired heavily by ranch. Two properties define it. It is written entirely in Elixir and is nearly dependency free, with telemetry as the only library it uses. And it is built as a supervision tree intended to be hosted inside a host application, often as a dependency embedded within a higher-level protocol library such as Bandit. That second point is the one that decides where the library belongs. It is not a server binary you start, it is a set of processes you place under your own supervisor and configure through start_link options. The project states the goal plainly: as capable as it is of backing demanding services, it should also work as an approachable reference for idiomatic OTP design patterns, and the same design has to serve both audiences.

Your side of the contract is one behaviour and one callback

Applications interact with Thousand Island through the ThousandIsland.Handler behaviour, which defines the interface the library uses to pass ThousandIsland.Socket structures up to the application level. Together, the behaviour and the socket type are the primary interface for almost every user of the library. A handler module declares the behaviour, implements handle_data with the data, the socket and the state, and returns the next state. Returning {:continue, state} keeps the connection open for more data, which is the one line that determines whether a handler is a loop or a one-shot. A simple implementation, given in the project documentation, looks like this:

elixir
defmodule Echo do
  use ThousandIsland.Handler

  @impl ThousandIsland.Handler
  def handle_data(data, socket, state) do
    ThousandIsland.Socket.send(socket, data)
    {:continue, state}
  end
end

{:ok, pid} = ThousandIsland.start_link(port: 1234, handler_module: Echo)

Four example handlers ship with the project in the examples folder, including echo.ex, daytime.ex, http_hello_world.ex and messenger.ex, which is the fastest way to see what a real handler looks like before you write your own.

A hundred acceptor supervisors, each nearly on its own

The implementation notes describe a hierarchy that is worth understanding before you tune anything. A Server process coordinates the work and manages two top-level processes: a Listener that binds the port and manages the listener socket, and an AcceptorPoolSupervisor that manages a pool of AcceptorSupervisor processes. There are 100 of those by default, and each one manages exactly two processes, an Acceptor that accepts connections and a DynamicSupervisor holding the processes that back individual client connections. When a client connects, one of the Acceptors receives the socket, creates a new process based on the configured handler, and immediately waits for the next connection. Acceptors are long lived and normally live for as long as the Server runs, while a handler process is tied to the lifecycle of the client connection and exists only while that connection is up. The hierarchy exists for two stated reasons: connections spend less time waiting to be accepted, and there is less contention on the DynamicSupervisor when handler processes are created, because each supervisor subtree works almost autonomously.

Shutdown is a drain with a 15000 ms budget

The shutdown story is more careful than most socket servers manage, and it is worth reading before you put this in production. The Server process is a standard Supervisor, so the usual rules about shutdown and shutdown timeouts apply. The moment the shutdown sequence begins, the ThousandIsland.ShutdownListener causes the listening socket to shut down, which in turn shuts down all of the Acceptor processes. What remains in the tree are several layers of Supervisors and whichever Handler processes were in progress at that moment. Standard Supervisor shutdown timeout semantics then give existing connections a chance to finish, and because Handler processes trap exit they keep running past the shutdown until they either complete their work or are killed with :brutal_kill once the timeout expires. The shutdown_timeout configuration option gives fine grained control over that value, and it defaults to 15000 ms. The practical implication is that a stuck handler can delay termination by that whole budget.

No inline logging, so tracing comes from telemetry

Thousand Island deliberately does no inline logging of any kind, which is an unusual choice for a server library and one that deserves a decision rather than an assumption. The reasoning is stated: it is a low-level library, so logging belongs to the host application. What it provides instead is the ThousandIsland.Logger module, a set of functions that aid in tracing connections at various log levels, and that logging can be enabled and disabled dynamically against an already running server, which matters when you need to look at one connection without restarting the service. Internally that logging is backed by telemetry events. The library emits a rich set of those events, including spans for each server, each acceptor process and each individual client connection, and they are documented in the ThousandIsland.Telemetry module. The single dependency in the project, the telemetry library, exists for exactly this reason.

Starting a server and where to look for the options

Servers are started with a call to ThousandIsland.start_link/1, which takes an options map, and the documented minimal call is the one in the example above, a port and a handler_module. The project documentation points at the ThousandIsland options type for the complete list, and the API docs live on hexdocs.pm rather than in the repository, so the option set is not something you can read from the source tree in one place. The repository itself is laid out like a normal Elixir library project: mix.exs and mix.lock at the root, the library under lib/, tests under test/, an examples folder with the four sample handlers, plus the tooling a Hex package usually carries, including .credo.exs for the Credo static analysis, .dialyzer_ignore.exs for Dialyzer exceptions and .formatter.exs for code formatting. There is also a SECURITY.md and a CHANGELOG.md. No GitHub releases have been published for the project, so the Hex package and the changelog are where release information lives.

The ranch comparison, and what the README does not settle

The project's own comparison with ranch comes with a hedge worth reading carefully. The README says informal tests place ranch and Thousand Island at roughly the same level of performance and overhead, and that short of synthetic scenarios on the busiest of servers they perform equally for all intents and purposes. That is the project's own claim, based on informal tests, and the README publishes no benchmark numbers, no machine specification and no methodology, so it supports the conclusion that the two are in the same class rather than any comparison of specific figures. The other difference the README does state is architectural: Thousand Island is written entirely in Elixir and is nearly dependency free, and it is presented as both a capable server and a readable reference for OTP design. What the README does not cover is TLS configuration, even though the repository topics list tls among the tags, and it does not cover protocol-level features, because that is not what this library is for.

Editorial conclusion

Thousand Island fits Elixir teams that want a socket layer they can read end to end, that need connection draining with a predictable timeout, and that would rather own one behaviour than a framework's configuration surface. It does not fit a project looking for a protocol library, since it stops at raw sockets and expects a higher-level library such as Bandit to sit on top of it, and it does not fit a team that wants a documented TLS story, since the repository topics mention tls while the README covers no TLS options. Before adopting it, check three things: the shutdown behaviour, where handlers trap exit and are eventually killed after the shutdown_timeout that defaults to 15000 ms; the handler interface, since handle_data returning {:continue, state} is what keeps a connection alive; and the absence of inline logging, since tracing has to be turned on through ThousandIsland.Logger or read from the telemetry events. The last push was on 2026-10-15, the licence is MIT, and no GitHub releases have been published.

Frequently asked questions

What is Thousand Island and what does it replace?

Thousand Island is a pure Elixir socket server inspired by ranch, built as an OTP supervision tree that you host inside your own application. The README states that informal tests place ranch and Thousand Island at roughly the same level of performance and overhead.

How do I write a handler for a Thousand Island server?

Define a module that uses ThousandIsland.Handler and implements handle_data with the data, the socket and the state, then return {:continue, state} to keep the connection open. The socket is a ThousandIsland.Socket, and you send on it with ThousandIsland.Socket.send(socket, data).

How do I start a Thousand Island server?

Call ThousandIsland.start_link/1 with an options map, for example a port and a handler_module, and the server is started as a supervision tree that you keep under your own supervisor. The complete option list is documented in the options type on the project's hexdocs page.

What happens to open connections when the server shuts down?

The shutdown listener closes the listening socket, which shuts down the acceptor processes, and then standard Supervisor shutdown timeout semantics give existing connections a chance to finish. Handler processes trap exit and keep running until they complete or are killed with :brutal_kill when the shutdown timeout expires, and that timeout defaults to 15000 ms.

How do I log connections in a Thousand Island server?

The library does no inline logging by design. Use the ThousandIsland.Logger module to trace connections at various log levels, and you can enable or disable that logging dynamically against a running server, with the events themselves backed by telemetry, including spans for each server, acceptor process and client connection.

Official sources

  1. Issues
  2. License: MIT
  3. mtrudel/thousand_island on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mtrudel-thousand-island.svg)](https://hysenlabs.com/projects/mtrudel-thousand-island)

Was this useful?