Plug: composing Elixir web applications from functions, not frameworks
Compose web applications with functions
At a glance
- What is it?
- Plug is a specification for building HTTP pipelines out of small functions, plus adapters that let the same pipeline run on Cowboy or Bandit. It is the layer Phoenix sits on, and it is usable on its own.
- Who is it for?
- Adopt Plug if you want HTTP handling expressed as an explicit list of functions you can read top to bottom, or if you are writing a library that must run on more than one Erlang VM server. Do not adopt it expecting a framework: routing, templating, database access and asset handling are outside its scope, and the README points at Phoenix for that.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 26 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 September 29, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
What Plug actually is, and who reaches for it
Plug occupies the layer between an Erlang VM web server and your application code. The README states it plainly: Plug is a specification for composing web applications with functions, and a set of connection adapters for different web servers in the Erlang VM. That is two products in one repository, and the split matters. The specification part is a contract about two functions, init/1 and call/2. The adapter part is the code that translates a real socket into a %Plug.Conn{} struct and translates the struct back into a response.
The audience is narrower than "Elixir developers". If you are building a Phoenix application, you are already running Plug on every request, but you rarely write a plug by hand unless you need authentication, request logging, body parsing or a header rewrite. The people who reach for Plug directly are writing small HTTP services, internal APIs, webhook receivers, or libraries that must not depend on a framework. The README names Phoenix as a consumer, which tells you the abstraction has held up under a large application, but it also tells you that Plug alone is not a framework. There is no ORM, no template engine, no asset pipeline, no code generator.
Module plugs, function plugs, and the immutable connection
The unit of composition is a plug. The README defines two shapes. A module plug implements init/1, which receives options and returns the options that call/2 will later receive, plus call/2, which takes the connection and those options and returns a connection. A function plug is simply a function that takes the connection and options and returns a connection. Both are interchangeable in a pipeline, which is why the same list can mix Plug.Logger, a router, and a four-line function you wrote this morning.
Data flows through a %Plug.Conn{} struct. The README shows it carrying host and path_info fields, and notes that you can read fields directly or pattern match on them. The design constraint that shapes everything else is immutability. The README is explicit: a connection is immutable, so every manipulation returns a new copy. The idiomatic form is rebinding, as in conn = put_resp_content_type(conn, "text/plain") followed by conn = send_resp(conn, 200, "ok").
That choice has a visible cost. Every plug in a long pipeline produces a new struct rather than mutating one, and a plug that forgets to return the connection it was given silently drops the work of every plug before it. The benefit is that a pipeline is a value you can reason about, and that a plug cannot reach back and change something an earlier plug already decided. If you have debugged middleware that mutated shared state in another ecosystem, the trade is legible.
Installing Plug and running a first request
Plug does not ship a web server. The README is direct about this: to use Plug you need a webserver and its bindings for Plug, and it lists two options, plug_cowboy for the Erlang-based Cowboy server and bandit for the Elixir-based Bandit server. Depend on one of them; the plug package itself is pulled in transitively, but the README's examples add it explicitly.
The fastest path is a single script file using Mix.install/1, which resolves dependencies at runtime. This is the README's hello world, using Cowboy on port 4000:
Mix.install([:plug, :plug_cowboy])
defmodule MyPlug do
import Plug.Conn
def init(options), do: options
def call(conn, _opts) do
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, "Hello world")
end
endSave the full snippet to hello_world.exs and run it as elixir hello_world.exs. Visiting http://localhost:4000/ should return the text. The README notes that the example starts the server in a throw-away supervisor and that production deployments should use an application supervision tree instead.
For that production shape, the README's sequence is to create a project with mix new my_app --sup, add the adapter to deps in mix.exs, and list the server as a child in lib/my_app/application.ex:
def deps do
[
{:plug_cowboy, "~> 2.0"}
]
endchildren = [
{Plug.Cowboy, scheme: :http, plug: MyPlug, options: [port: 4001]}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)Running mix run --no-halt then starts the application with a server on http://localhost:4001. The only difference between the two servers at boot time is the child spec: Plug.Cowboy or Bandit. Everything above that line is unchanged, which is the whole point of the adapter split.
Routing and WebSockets without leaving the library
Plug.Router is the built-in answer to dispatch. The README's WebSocket example uses it, with plug Plug.Logger, plug :match and plug :dispatch as the pipeline, then get "/" and get "/websocket" clauses. The :match and :dispatch entries are not decoration; they are the two stages that select a route and then execute its body, and omitting them means your routes never run. That is a real trap for someone copying a router example and trimming what looks like boilerplate.
WebSocket support arrives through the connection upgrade API introduced in Plug v1.14, according to the README. The upgrade itself is delegated to a separate package, websock_adapter, and the example calls WebSockAdapter.upgrade(EchoServer, [], timeout: 60_000) and then halts the connection. The handler module implements init/1, handle_in/2 and terminate/2. So the claim "WebSocket support out of the box" needs a qualifier: the connection upgrade mechanism is in Plug, and the protocol implementation is a dependency you add.
The README's example runs on Bandit rather than Cowboy, which is a reasonable illustration of the abstraction but also a reminder that the two adapters are separate codebases with separate release cadences. Choosing one is a decision you live with.
Where Plug is the wrong tool
Plug gives you a request and a response. It does not give you a session store, a database layer, a template engine, form helpers, CSRF protection wired to a session, or a migration system. If your project needs any of those on day one, you are choosing Phoenix or another framework, and the README's own framing supports that: it describes Phoenix as a consumer of Plug, not a competitor.
The second limitation is subtler. Because a plug pipeline is a list of functions with no declared ordering constraints, nothing prevents you from putting an authentication plug after the plug that already sent a response. The connection is immutable, but send_resp/3 marks it as sent, and the README's WebSocket example ends with halt() precisely because control flow past a response is a hazard you manage by convention. There is no type-level guarantee that your pipeline is ordered correctly.
The third case is scale of team. A pipeline of twenty plugs is readable. A pipeline of two hundred, spread across a codebase with no framework conventions for where plugs live and what they may depend on, is not obviously better than a framework's opinionated structure. Plug optimizes for small, explicit composition, and it does not pretend otherwise.
Plug against a full framework, and against raw Cowboy
The realistic alternative is Phoenix. The difference is not performance or philosophy at the HTTP layer, since Phoenix runs on Plug. The difference is what comes in the box. Phoenix adds routing with compile-time path verification, controllers with a conventional request lifecycle, channels, LiveView, Ecto integration, and generators that produce the file layout. Choosing Plug means choosing to assemble those pieces yourself or to leave them out. For a service that exposes four JSON endpoints and reads from one external API, assembling is cheap. For an application with user accounts, database migrations and server-rendered pages, assembling is a project of its own.
The other comparison is against writing directly against Cowboy's own Erlang API. Plug's value there is the adapter boundary: code written against %Plug.Conn{} does not know which server is underneath, so moving from Plug.Cowboy to Bandit is a change to one child spec. That portability is the concrete thing you buy, and it is the reason a library author would target Plug rather than a specific server.
Maintenance, licensing and what a version bump costs you
The repository is not archived, and the last push was on 2026-09-03. The README references Plug v1.14 as the release that introduced the connection upgrade API, and the dependency examples pin plug_cowboy at ~> 2.0 and bandit at ~> 1.0. Those are the version constraints the project itself publishes.
The upgrade cost is concentrated in the adapter, not in Plug. Because the specification is two functions with stable signatures, application plugs rarely need changes across Plug releases. The packages that track server internals, plug_cowboy and bandit, move on their own schedules, and a Cowboy major version is a Cowboy concern. If you want a low-churn dependency, the adapter choice is the variable to watch.
On licensing, the repository carries a LICENSE file and GitHub reports the license as NOASSERTION, meaning its classifier could not map the file to a known identifier. The README says nothing about licensing terms. Read LICENSE directly and have whoever handles compliance make the call; nothing in the documentation substitutes for that.
Editorial conclusion
Adopt Plug if you want HTTP handling expressed as an explicit list of functions you can read top to bottom, or if you are writing a library that must run on more than one Erlang VM server. Do not adopt it expecting a framework: routing, templating, database access and asset handling are outside its scope, and the README points at Phoenix for that. Before committing, verify two things in the repository itself: which adapter you will depend on (plug_cowboy or bandit, they are separate packages) and whether your deployment starts the server inside an application supervision tree rather than a throw-away supervisor, since the README treats the supervised form as the production one.
Frequently asked questions
What is an elixir used for?
The README does not describe the Elixir language itself; it documents Plug, a specification for composing web applications with functions and a set of connection adapters for web servers in the Erlang VM. Plug is written in Elixir and is used by frameworks such as Phoenix.
Is Elixir backend or frontend?
The README does not address this directly, but everything it documents runs on the Erlang VM: Plug composes request and response handling and connects to servers such as Cowboy and Bandit. The WebSocket example still serves an HTTP endpoint that a browser connects to.
Do I need Cowboy to use Plug?
No. The README lists two options: the Erlang-based Cowboy server via the plug_cowboy package, or the Elixir-based Bandit server via the bandit package. The only difference at boot is whether the child spec names Plug.Cowboy or Bandit.
How do I install Plug for an Elixir project?
Add an adapter to your deps in mix.exs, either {:plug_cowboy, "~> 2.0"} or {:bandit, "~> 1.0"}, and for production start it as a child in your application supervision tree. The README's production example creates the project with mix new my_app --sup and runs it with mix run --no-halt.
Does Plug support WebSockets?
Plug v1.14 includes a connection upgrade API, and the README's example delegates the protocol work to the websock_adapter package, calling WebSockAdapter.upgrade with a handler module that implements init/1, handle_in/2 and terminate/2. The upgrade mechanism is in Plug; the WebSocket implementation is a separate dependency.
Official sources
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.
[](https://hysenlabs.com/projects/elixir-plug-plug)
Community notes