An Elixir implementation of gRPC
1,522
stars
647
commits
Elixir
primary language
Sep 8, 2026
updated
gRPC Elixir is a full-featured Elixir implementation of the gRPC protocol, supporting unary and streaming RPCs, interceptors, HTTP transcoding, and TLS. This version adopts a unified stream-based model for all types of calls.
The package can be installed as:
def deps do
[
{:grpc, "~> 0.11"},
{:grpc_server, "~> 0.11"},
{:protobuf, "~> 0.14"}, # optional for import wellknown google types
{:grpc_reflection, "~> 0.2"} # optional enable grpc reflection
]
end
Note (since v1.0.0-rc.1): To use the server, you must add the :grpc_server dependency to your mix.exs instead of the older :grpc package.
Use protoc with protobuf elixir plugin or using protobuf_generate hex package to generate the necessary files.
syntax = "proto3";
package helloworld;
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greeting
message HelloReply {
string message = 1;
}
// The greeting service definition.
service GreetingServer {
rpc SayUnaryHello (HelloRequest) returns (HelloReply) {}
rpc SayServerHello (HelloRequest) returns (stream HelloReply) {}
rpc SayBidStreamHello (stream HelloRequest) returns (stream HelloReply) {}
}
protoc --elixir_out=plugins=grpc:./lib -I./priv/protos helloworld.proto
All RPC calls must be implemented using the stream-based API, even for unary requests.
Note: The old API was deprecated based on
GRPC.Server.send_reply/2and directstructreturns was deprecated as of version0.10.x.
defmodule HelloworldStreams.Server do
use GRPC.Server, service: Helloworld.GreetingServer.Service
alias GRPC.Stream
alias Helloworld.HelloRequest
alias Helloworld.HelloReply
def say_unary_hello(request, materializer) do
request
|> GRPC.Stream.unary(materializer: materializer)
|> GRPC.Stream.map(fn %HelloReply{} = reply ->
%HelloReply{message: "[Reply] #{reply.message}"}
end)
|> GRPC.Stream.run()
end
end
def say_server_hello(request, materializer) do
Stream.repeatedly(fn ->
index = :rand.uniform(10)
%HelloReply{message: "[#{index}] Hello #{request.name}"}
end)
|> Stream.take(10)
|> GRPC.Stream.from()
|> GRPC.Stream.run_with(materializer)
end
def say_bid_stream_hello(request, materializer) do
output_stream =
Stream.repeatedly(fn ->
index = :rand.uniform(10)
%HelloReply{message: "[#{index}] Server response"}
end)
GRPC.Stream.from(request, join_with: output_stream)
|> GRPC.Stream.map(fn
%HelloRequest{name: name} -> %HelloReply{message: "Welcome #{name}"}
other -> other
end)
|> GRPC.Stream.run_with(materializer)
end
The Stream API supports composable stream transformations via ask, map, run and others functions, enabling clean and declarative stream pipelines. For a complete list of available operators see here.
The effect/2 operator executes user-defined functions for each element in the stream, allowing the integration of non-transformative actions such as logging, metrics, or external notifications.
Unlike transformation operators (e.g., map/2), effect/2 does not modify or filter values — it preserves the original stream while executing the provided callback safely for each emitted element.
iex> parent = self()
iex> stream =
...> GRPC.Stream.from([1, 2, 3])
...> |> GRPC.Stream.effect(fn x -> send(parent, {:seen, x * 2}) end)
...> |> GRPC.Stream.to_flow()
...> |> Enum.to_list()
iex> assert_receive {:seen, 2}
iex> assert_receive {:seen, 4}
iex> assert_receive {:seen, 6}
iex> stream
[1, 2, 3]
Key characteristics:
effect_fun) is invoked for each item emitted downstream.This operator is designed for observability, telemetry, auditing, and integration with external systems that must react to events flowing through the gRPC stream.
The map_error/2 operator intercepts and transforms errors or exceptions emitted by previous stages in a stream pipeline.
It provides a unified mechanism for handling:
{:error, reason})iex> GRPC.Stream.from([1, 2])
...> |> GRPC.Stream.map(fn
...> 2 -> raise "boom"
...> x -> x
...> end)
...> |> GRPC.Stream.map_error(fn
...> {:error, {:exception, _reason}} ->
...> {:error, GRPC.RPCError.exception(message: "Booomm")}
...> end)
In this example:
map/2 raises an exception for the value 2.map_error/2 captures and transforms that error into a structured GRPC.RPCError response.This makes map_error/2 suitable for input validation, runtime fault recovery, and user-facing error translation within gRPC pipelines.
All stream operators share a unified error propagation model that guarantees consistent handling of exceptions and failures across the pipeline.
This ensures that user-defined functions within the stream — whether pure transformations, side effects, or external calls — always produce a predictable and recoverable result, maintaining the integrity of the dataflow even in the presence of unexpected errors.
def say_unary_hello(request, _materializer) do
GRPCStream.unary(request)
|> GRPCStream.ask(Transformer)
|> GRPCStream.map(fn
%HelloReply{} = reply ->
%HelloReply{message: "[Reply] #{reply.message}"}
{:error, reason} ->
{:error, GRPC.RPCError.exception(message: "error calling external process: #{inspect(reason)}")}
error ->
Logger.error("Unknown error")
error
end)
|> GRPCStream.run()
end
By normalizing all possible outcomes, GRPC.Stream ensures fault-tolerant, exception-safe pipelines where operators can freely raise, throw, or return tuples without breaking the flow execution.
This unified model allows developers to build composable and reliable streaming pipelines that gracefully recover from both domain and runtime errors.
NOTE: In the example above, we could use
map_error/2instead ofmap/2to handle error cases explicitly. However, since the function also performs a transformation on successful values,map/2remains appropriate and useful in this context.
Add the server supervisor to your application's supervision tree:
defmodule Helloworld.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
GrpcReflection,
{
GRPC.Server.Supervisor, [
endpoint: Helloworld.Endpoint,
port: 50051,
start_server: true,
# adapter_opts: [# any adapter-specific options like tls configuration....]
]
}
]
opts = [strategy: :one_for_one, name: Helloworld.Supervisor]
Supervisor.start_link(children, opts)
end
end
This section demonstrates how to establish client connections and perform RPC calls using the Elixir gRPC client.
Note (since v1.0.0-rc.1): To use the client, you must explicitly add the :grpc dependency to your mix.exs.
Typically, you start this client supervisor as part of your application's supervision tree:
children = [
{GRPC.Client.Supervisor, []}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
You can also start it manually in scripts or test environments:
{:ok, _pid} = DynamicSupervisor.start_link(strategy: :one_for_one, name: GRPC.Client.Supervisor)
Then connect with gRPC server:
iex> {:ok, channel} = GRPC.Stub.connect("localhost:50051")
iex> request = Helloworld.HelloRequest.new(name: "grpc-elixir")
iex> {:ok, reply} = channel |> Helloworld.GreetingServer.Stub.say_unary_hello(request)
Client interceptors allow you to add logic to the request/response lifecycle, such as logging, tracing, or authentication.
iex> {:ok, channel} =
...> GRPC.Stub.connect("localhost:50051",
...> interceptors: [GRPC.Client.Interceptors.Logger]
...> )
iex> request = Helloworld.HelloRequest.new(name: "Alice")
iex> {:ok, reply} = channel |> Helloworld.GreetingServer.Stub.say_unary_hello(request)
The connect/2 function supports URI-like targets that are resolved via the internal gRPC Resolver.
You can connect using DNS, Unix Domain sockets, IPv4/IPv6, or even xDS-based endpoints.
| Scheme | Example | Description |
|---|---|---|
dns:// | "dns://example.com:50051" | Resolves via DNS A/AAAA records |
ipv4: | "ipv4:10.0.0.5:50051" | Connects directly to an IPv4 address |
unix: | "unix:/tmp/service.sock" | Connects via a Unix domain socket |
xds:/// | "xds:///my-service" | Resolves via xDS control plane (Envoy/Istio) |
| none | "127.0.0.1:50051" | Implicit DNS (default port 50051) |
iex> {:ok, channel} = GRPC.Stub.connect("dns://orders.prod.svc.cluster.local:50051")
iex> request = Orders.GetOrderRequest.new(id: "123")
iex> {:ok, reply} = channel |> Orders.OrderService.Stub.get_order(request)
iex> {:ok, channel} = GRPC.Stub.connect("unix:/tmp/my.sock")
NOTE: When using
DNSorxDStargets, the connection layer periodically refreshes endpoints.
You can specify message compression and attach default headers to all requests.
iex> {:ok, channel} =
...> GRPC.Stub.connect("localhost:50051",
...> compressor: GRPC.Compressor.Gzip,
...> headers: [{"authorization", "Bearer my-token"}]
...> )
By default, GRPC.Stub.connect/2 uses the Gun adapter.
You can switch to Mint (pure Elixir HTTP/2) or other adapters as needed.
Since both of these are optional dependencies, you must add one to your dependencies list:
def deps do
[
# Gun
{:gun, "~> 2.0"},
# Mint
{:mint, "~> 1.5"}
]
end
iex> GRPC.Stub.connect("localhost:50051",
...> adapter: GRPC.Client.Adapters.Mint
...> )
You can configure adapter options globally via your application’s config:
# File: config/config.exs
config :grpc, GRPC.Client.Adapters.Mint,
timeout: 10_000,
transport_opts: [cacertfile: "/etc/ssl/certs/ca-certificates.crt"]
The accepted options are the same as Mint.HTTP.connect/4.
The Mint adapter supports automatic reconnection when the underlying HTTP/2 connection drops (e.g. server restart, network interruption). To enable it, pass the :retry option via adapter_opts:
iex> {:ok, channel} = GRPC.Stub.connect("localhost:50051",
...> adapter: GRPC.Client.Adapters.Mint,
...> adapter_opts: [retry: 5]
...> )
When the connection drops, the adapter will attempt to reconnect up to retry times using exponential backoff with jitter. The delay starts at ~1 second and grows up to a maximum of 120 seconds. If all attempts are exhausted, the parent process receives a {:elixir_grpc, :connection_down, pid} message.
By default, :retry is 0 (no reconnection attempts).
Note: Any in-flight requests at the time of the drop will fail immediately. Reconnection only re-establishes the transport connection — it does not replay requests.
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
get: "/v1/greeter/{name}"
};
}
rpc SayHelloFrom (HelloRequestFrom) returns (HelloReply) {
option (google.api.http) = {
post: "/v1/greeter"
body: "*"
};
}
}
In mix.exs:
def deps do
[
{:grpc, "~> 0.11"},
{:protobuf_generate, "~> 0.1.3"}
]
end
And in your terminal:
mix protobuf.generate \
--include-path=priv/proto \
--include-path=deps/googleapis \
--generate-descriptors=true \
--output-path=./lib \
--plugins=ProtobufGenerate.Plugins.GRPCWithOptions \
google/api/annotations.proto google/api/http.proto helloworld.proto
defmodule Helloworld.Greeter.Server do
use GRPC.Server,
service: Helloworld.Greeter.Service,
http_transcode: true
# callback implementations...
end
See full application code in helloworld_transcoding example.
When accessing gRPC from a browser via HTTP transcoding or gRPC-Web, CORS headers may be required for the browser to allow access to the gRPC endpoint. Adding CORS headers can be done by using GRPC.Server.Interceptors.CORS as an interceptor in your GRPC.Endpoint module, configuring it as described in the module documentation:
Example:
# Define your endpoint
defmodule Helloworld.Endpoint do
use GRPC.Endpoint
intercept GRPC.Server.Interceptors.Logger
intercept GRPC.Server.Interceptors.CORS, allow_origin: "mydomain.io"
run Helloworld.Greeter.Server
end
Simple benchmark by using ghz
Benchmark followed by official spec
Your contributions are welcome!
Please open issues if you have questions, problems and ideas. You can create pull requests directly if you want to fix little bugs, add small features and so on. But you'd better use issues first if you want to add a big feature or change a lot of code.
(top 30 of 87)
Elixir
98.3%
Erlang
1.4%
An Elixir implementation of gRPC
1,522
stars
647
commits
Elixir
primary language
Sep 8, 2026
updated
gRPC Elixir is a full-featured Elixir implementation of the gRPC protocol, supporting unary and streaming RPCs, interceptors, HTTP transcoding, and TLS. This version adopts a unified stream-based model for all types of calls.
The package can be installed as:
def deps do
[
{:grpc, "~> 0.11"},
{:grpc_server, "~> 0.11"},
{:protobuf, "~> 0.14"}, # optional for import wellknown google types
{:grpc_reflection, "~> 0.2"} # optional enable grpc reflection
]
end
Note (since v1.0.0-rc.1): To use the server, you must add the :grpc_server dependency to your mix.exs instead of the older :grpc package.
Use protoc with protobuf elixir plugin or using protobuf_generate hex package to generate the necessary files.
syntax = "proto3";
package helloworld;
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greeting
message HelloReply {
string message = 1;
}
// The greeting service definition.
service GreetingServer {
rpc SayUnaryHello (HelloRequest) returns (HelloReply) {}
rpc SayServerHello (HelloRequest) returns (stream HelloReply) {}
rpc SayBidStreamHello (stream HelloRequest) returns (stream HelloReply) {}
}
protoc --elixir_out=plugins=grpc:./lib -I./priv/protos helloworld.proto
All RPC calls must be implemented using the stream-based API, even for unary requests.
Note: The old API was deprecated based on
GRPC.Server.send_reply/2and directstructreturns was deprecated as of version0.10.x.
defmodule HelloworldStreams.Server do
use GRPC.Server, service: Helloworld.GreetingServer.Service
alias GRPC.Stream
alias Helloworld.HelloRequest
alias Helloworld.HelloReply
def say_unary_hello(request, materializer) do
request
|> GRPC.Stream.unary(materializer: materializer)
|> GRPC.Stream.map(fn %HelloReply{} = reply ->
%HelloReply{message: "[Reply] #{reply.message}"}
end)
|> GRPC.Stream.run()
end
end
def say_server_hello(request, materializer) do
Stream.repeatedly(fn ->
index = :rand.uniform(10)
%HelloReply{message: "[#{index}] Hello #{request.name}"}
end)
|> Stream.take(10)
|> GRPC.Stream.from()
|> GRPC.Stream.run_with(materializer)
end
def say_bid_stream_hello(request, materializer) do
output_stream =
Stream.repeatedly(fn ->
index = :rand.uniform(10)
%HelloReply{message: "[#{index}] Server response"}
end)
GRPC.Stream.from(request, join_with: output_stream)
|> GRPC.Stream.map(fn
%HelloRequest{name: name} -> %HelloReply{message: "Welcome #{name}"}
other -> other
end)
|> GRPC.Stream.run_with(materializer)
end
The Stream API supports composable stream transformations via ask, map, run and others functions, enabling clean and declarative stream pipelines. For a complete list of available operators see here.
The effect/2 operator executes user-defined functions for each element in the stream, allowing the integration of non-transformative actions such as logging, metrics, or external notifications.
Unlike transformation operators (e.g., map/2), effect/2 does not modify or filter values — it preserves the original stream while executing the provided callback safely for each emitted element.
iex> parent = self()
iex> stream =
...> GRPC.Stream.from([1, 2, 3])
...> |> GRPC.Stream.effect(fn x -> send(parent, {:seen, x * 2}) end)
...> |> GRPC.Stream.to_flow()
...> |> Enum.to_list()
iex> assert_receive {:seen, 2}
iex> assert_receive {:seen, 4}
iex> assert_receive {:seen, 6}
iex> stream
[1, 2, 3]
Key characteristics:
effect_fun) is invoked for each item emitted downstream.This operator is designed for observability, telemetry, auditing, and integration with external systems that must react to events flowing through the gRPC stream.
The map_error/2 operator intercepts and transforms errors or exceptions emitted by previous stages in a stream pipeline.
It provides a unified mechanism for handling:
{:error, reason})iex> GRPC.Stream.from([1, 2])
...> |> GRPC.Stream.map(fn
...> 2 -> raise "boom"
...> x -> x
...> end)
...> |> GRPC.Stream.map_error(fn
...> {:error, {:exception, _reason}} ->
...> {:error, GRPC.RPCError.exception(message: "Booomm")}
...> end)
In this example:
map/2 raises an exception for the value 2.map_error/2 captures and transforms that error into a structured GRPC.RPCError response.This makes map_error/2 suitable for input validation, runtime fault recovery, and user-facing error translation within gRPC pipelines.
All stream operators share a unified error propagation model that guarantees consistent handling of exceptions and failures across the pipeline.
This ensures that user-defined functions within the stream — whether pure transformations, side effects, or external calls — always produce a predictable and recoverable result, maintaining the integrity of the dataflow even in the presence of unexpected errors.
def say_unary_hello(request, _materializer) do
GRPCStream.unary(request)
|> GRPCStream.ask(Transformer)
|> GRPCStream.map(fn
%HelloReply{} = reply ->
%HelloReply{message: "[Reply] #{reply.message}"}
{:error, reason} ->
{:error, GRPC.RPCError.exception(message: "error calling external process: #{inspect(reason)}")}
error ->
Logger.error("Unknown error")
error
end)
|> GRPCStream.run()
end
By normalizing all possible outcomes, GRPC.Stream ensures fault-tolerant, exception-safe pipelines where operators can freely raise, throw, or return tuples without breaking the flow execution.
This unified model allows developers to build composable and reliable streaming pipelines that gracefully recover from both domain and runtime errors.
NOTE: In the example above, we could use
map_error/2instead ofmap/2to handle error cases explicitly. However, since the function also performs a transformation on successful values,map/2remains appropriate and useful in this context.
Add the server supervisor to your application's supervision tree:
defmodule Helloworld.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
GrpcReflection,
{
GRPC.Server.Supervisor, [
endpoint: Helloworld.Endpoint,
port: 50051,
start_server: true,
# adapter_opts: [# any adapter-specific options like tls configuration....]
]
}
]
opts = [strategy: :one_for_one, name: Helloworld.Supervisor]
Supervisor.start_link(children, opts)
end
end
This section demonstrates how to establish client connections and perform RPC calls using the Elixir gRPC client.
Note (since v1.0.0-rc.1): To use the client, you must explicitly add the :grpc dependency to your mix.exs.
Typically, you start this client supervisor as part of your application's supervision tree:
children = [
{GRPC.Client.Supervisor, []}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
You can also start it manually in scripts or test environments:
{:ok, _pid} = DynamicSupervisor.start_link(strategy: :one_for_one, name: GRPC.Client.Supervisor)
Then connect with gRPC server:
iex> {:ok, channel} = GRPC.Stub.connect("localhost:50051")
iex> request = Helloworld.HelloRequest.new(name: "grpc-elixir")
iex> {:ok, reply} = channel |> Helloworld.GreetingServer.Stub.say_unary_hello(request)
Client interceptors allow you to add logic to the request/response lifecycle, such as logging, tracing, or authentication.
iex> {:ok, channel} =
...> GRPC.Stub.connect("localhost:50051",
...> interceptors: [GRPC.Client.Interceptors.Logger]
...> )
iex> request = Helloworld.HelloRequest.new(name: "Alice")
iex> {:ok, reply} = channel |> Helloworld.GreetingServer.Stub.say_unary_hello(request)
The connect/2 function supports URI-like targets that are resolved via the internal gRPC Resolver.
You can connect using DNS, Unix Domain sockets, IPv4/IPv6, or even xDS-based endpoints.
| Scheme | Example | Description |
|---|---|---|
dns:// | "dns://example.com:50051" | Resolves via DNS A/AAAA records |
ipv4: | "ipv4:10.0.0.5:50051" | Connects directly to an IPv4 address |
unix: | "unix:/tmp/service.sock" | Connects via a Unix domain socket |
xds:/// | "xds:///my-service" | Resolves via xDS control plane (Envoy/Istio) |
| none | "127.0.0.1:50051" | Implicit DNS (default port 50051) |
iex> {:ok, channel} = GRPC.Stub.connect("dns://orders.prod.svc.cluster.local:50051")
iex> request = Orders.GetOrderRequest.new(id: "123")
iex> {:ok, reply} = channel |> Orders.OrderService.Stub.get_order(request)
iex> {:ok, channel} = GRPC.Stub.connect("unix:/tmp/my.sock")
NOTE: When using
DNSorxDStargets, the connection layer periodically refreshes endpoints.
You can specify message compression and attach default headers to all requests.
iex> {:ok, channel} =
...> GRPC.Stub.connect("localhost:50051",
...> compressor: GRPC.Compressor.Gzip,
...> headers: [{"authorization", "Bearer my-token"}]
...> )
By default, GRPC.Stub.connect/2 uses the Gun adapter.
You can switch to Mint (pure Elixir HTTP/2) or other adapters as needed.
Since both of these are optional dependencies, you must add one to your dependencies list:
def deps do
[
# Gun
{:gun, "~> 2.0"},
# Mint
{:mint, "~> 1.5"}
]
end
iex> GRPC.Stub.connect("localhost:50051",
...> adapter: GRPC.Client.Adapters.Mint
...> )
You can configure adapter options globally via your application’s config:
# File: config/config.exs
config :grpc, GRPC.Client.Adapters.Mint,
timeout: 10_000,
transport_opts: [cacertfile: "/etc/ssl/certs/ca-certificates.crt"]
The accepted options are the same as Mint.HTTP.connect/4.
The Mint adapter supports automatic reconnection when the underlying HTTP/2 connection drops (e.g. server restart, network interruption). To enable it, pass the :retry option via adapter_opts:
iex> {:ok, channel} = GRPC.Stub.connect("localhost:50051",
...> adapter: GRPC.Client.Adapters.Mint,
...> adapter_opts: [retry: 5]
...> )
When the connection drops, the adapter will attempt to reconnect up to retry times using exponential backoff with jitter. The delay starts at ~1 second and grows up to a maximum of 120 seconds. If all attempts are exhausted, the parent process receives a {:elixir_grpc, :connection_down, pid} message.
By default, :retry is 0 (no reconnection attempts).
Note: Any in-flight requests at the time of the drop will fail immediately. Reconnection only re-establishes the transport connection — it does not replay requests.
import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
get: "/v1/greeter/{name}"
};
}
rpc SayHelloFrom (HelloRequestFrom) returns (HelloReply) {
option (google.api.http) = {
post: "/v1/greeter"
body: "*"
};
}
}
In mix.exs:
def deps do
[
{:grpc, "~> 0.11"},
{:protobuf_generate, "~> 0.1.3"}
]
end
And in your terminal:
mix protobuf.generate \
--include-path=priv/proto \
--include-path=deps/googleapis \
--generate-descriptors=true \
--output-path=./lib \
--plugins=ProtobufGenerate.Plugins.GRPCWithOptions \
google/api/annotations.proto google/api/http.proto helloworld.proto
defmodule Helloworld.Greeter.Server do
use GRPC.Server,
service: Helloworld.Greeter.Service,
http_transcode: true
# callback implementations...
end
See full application code in helloworld_transcoding example.
When accessing gRPC from a browser via HTTP transcoding or gRPC-Web, CORS headers may be required for the browser to allow access to the gRPC endpoint. Adding CORS headers can be done by using GRPC.Server.Interceptors.CORS as an interceptor in your GRPC.Endpoint module, configuring it as described in the module documentation:
Example:
# Define your endpoint
defmodule Helloworld.Endpoint do
use GRPC.Endpoint
intercept GRPC.Server.Interceptors.Logger
intercept GRPC.Server.Interceptors.CORS, allow_origin: "mydomain.io"
run Helloworld.Greeter.Server
end
Simple benchmark by using ghz
Benchmark followed by official spec
Your contributions are welcome!
Please open issues if you have questions, problems and ideas. You can create pull requests directly if you want to fix little bugs, add small features and so on. But you'd better use issues first if you want to add a big feature or change a lot of code.
(top 30 of 87)
Elixir
98.3%
Erlang
1.4%