Gaoxy
Gaoxy runs supervised TCP, OTP TLS and UDP services inside an Elixir application.
Each instance owns its listeners and connection handlers. HTTP/Plug/Phoenix and
HTTP/3 are available through the separate gaoxy_http and gaoxy_http3 packages.
Install
Add to mix.exs, then run mix deps.get:
defp deps do
[
{:gaoxy, "~> 0.15.1"}
]
end
Use Elixir 1.18 or newer with Erlang/OTP. Core uses Hex abyss == 1.0.0-rc.4, which
has no bundled DHCP NIF or Rustler build dependency. Neither Rust/Cargo nor
abyss_raw is required for ordinary Gaoxy services.
Run a TCP service
Implement the stream handler in your application:
defmodule MyApp.Echo do
@behaviour Gaoxy.StreamHandler
@impl true
def init(_context, _options), do: {:ok, nil}
@impl true
def handle_data(bytes, state), do: {:send, bytes, state}
end
Add a Gaoxy instance to your application's supervision tree:
children = [
{Gaoxy,
name: MyApp.Transports,
services: [
%{
id: :echo,
transport: :tcp,
ip: {127, 0, 0, 1},
port: 4001,
handler: MyApp.Echo
}
]}
]
Supervisor.start_link(children, strategy: :one_for_one)
Start your application and connect with nc 127.0.0.1 4001. Bytes written to
the connection are echoed back. Input callbacks receive an ordered byte stream;
your handler must buffer and parse any application message framing.
handler_options: term supplies the second argument to init/2.
Optional terminate/2 handles connection cleanup.
Raw TCP/TLS connections reserve bounded receive, state and output credit before handler initialization, including credit to finish I/O during drain. Default limits require 905,139 bytes per connection. Explicit byte caps must fund that floor; larger configured socket buffers increase it. See the resource-budget migration guide for buffer settings and the exact calculation.
Inspect the instance from IEx:
instance = Process.whereis(MyApp.Transports)
Gaoxy.services(instance)
Gaoxy.info(instance, :echo)
Gaoxy.pause(instance, :echo)
Gaoxy.resume(instance, :echo)
Service IDs belong to an instance, so two instances can each have an :echo
service. Gaoxy.add/2 and Gaoxy.remove/3 manage services dynamically;
Gaoxy.stop/2 stops the instance. A cleanup deadline error can mean cleanup is
still in progress.
TLS and UDP
For TLS, use transport: :tls and supply
tls: %{certfile: "/path/to/cert.pem", keyfile: "/path/to/key.pem"} on the service.
TCP/TLS stream services use the same handler behaviour. TLS uses OTP :ssl;
TLS 1.3 is the default.
For UDP, implement Gaoxy.DatagramHandler:
defmodule MyApp.DatagramEcho do
@behaviour Gaoxy.DatagramHandler
@impl true
def handle_datagram(%{data: bytes}, _options), do: {:reply, bytes}
end
Use %{id: :udp_echo, transport: :udp, ip: {127, 0, 0, 1}, port: 4002, handler: MyApp.DatagramEcho} in services. Each admitted packet gets one
isolated callback; {:reply, bytes} sends to that packet's sender.
Configuration
The instance accepts :name, :services and :resource_budget. Services are
maps; unknown keys are rejected. Bind addresses are IP tuples, and port 0
requests an ephemeral port. Defaults bind loopback.
Adaptive memory admission shares one capacity pool across instances in the same
BEAM VM. Its soft limit is 50% of effective physical/cgroup capacity and its hard
limit is twice soft. Unreliable detection uses 256/512 MiB. Usage above soft
through hard remains admissible with rate-limited warnings. Explicit instance
thresholds use resource_budget: %{soft_limit: 268_435_456, hard_limit: 402_653_184};
the explicit hard value is never doubled and the shared VM pool still applies.
Service limits.max_connections, max_requests and max_streams are independent
quotas, defaulting to 16,384, 65,536 and 65,536. Explicit legacy max_active and
max_bytes remain undoubled ordinary-lease and transport/cleanup hard caps;
WebSocket retained bytes participate in shared memory, not the legacy transport
cap. Omitted historical numeric profiles no longer impose aggregate caps.
Protocol, process and OOM safety guards remain finite and independent.
Gaoxy.resource_budget(instance) exposes thresholds, retained/temporary usage,
active resource counters, rejection reasons, detection and shared VM totals.
These budgets describe owned resources and commitments, not total VM or kernel
memory. See the resource budget guide
for exact boundaries, telemetry and migration semantics.
TCP/TLS services accept hell_options, for example
[num_acceptors: 4, transport_options: [backlog: 2048, keepalive: true]].
UDP services accept abyss_options, for example [udp_buffer_size: 262_144].
These are strict backend-specific subsets; binding, TLS, resource and lifecycle
controls remain in their existing fields. See
backend tuning
for supported keys, defaults and acceptor budget division.
For HTTP and Phoenix, see the HTTP package and Phoenix guide. See capabilities for transport support and architecture for lifecycle and resource ownership.