# Bamboo for Elixir: pipes, adapters, and testable mail

> Bamboo splits email into plain functions that build messages and a mailer module that sends them, which is why it pipes well and tests well. Its weak points are a mailer configured through config keys and a dependency on Hackney underneath every adapter.

**beam-community/bamboo** — Testable, composable, and adapter based Elixir email library for devs that love piping.

- Repository: https://github.com/beam-community/bamboo
- Website: https://hex.pm/packages/bamboo
- Stars: 1,961 · Forks: 339
- Language: Elixir
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/beam-community-bamboo

## Two modules, and the split is the whole design

Bamboo's central decision is that email creation and email delivery are separate concerns handled by separate modules. You write a module that imports `Bamboo.Email` and returns a plain email struct from functions like `new_email/1`. You write a second module that does `use Bamboo.Mailer, otp_app: :my_app` and does nothing else. Sending is then a pipe from the first to the second.

```elixir
defmodule MyApp.SomeControllerPerhaps do
  def send_welcome_email do
    Email.welcome_email()   # Create your email
    |> Mailer.deliver_now!() # Send your email
  end
end
```

Two consequences follow from that split. The first is testability, and it is the reason the library has survived changes of maintainer. Because `welcome_email()` returns a value rather than performing a side effect, a test asserts on the email's fields without any special assertion helper for the act of sending. Because the mailer is a module you named, a test swaps the adapter and asserts on what was handed to it.

The second consequence is that the mailer is a configuration point, not a global. Configuration is keyed by OTP app and mailer module:

```elixir
config :my_app, MyApp.Mailer,
  adapter: Bamboo.MandrillAdapter,
  api_key: "my_api_key"
```

Your OTP application, your mailer module, the adapter, and whatever the adapter itself needs. Nothing is read from a Bamboo-owned config namespace, so you can have several mailers in one application with different adapters if your domain calls for it.

## Install: mix deps.get, and a version requirement worth noting

Adding Bamboo is a two-line change plus a fetch. The published dependency is given as a version requirement:

```elixir
def deps do
  [{:bamboo, "~> 2.3.0"}]
end
```

Then run `mix deps.get`. If you would rather track the main branch than a hex release, the README gives the git form instead, `{:bamboo, github: "beam-community/bamboo"}`, and warns in the same breath that this README follows the main branch, which may not be the currently published version. The published docs at hexdocs.pm are the reference for what you actually get from a hex dependency.

The release history is sparse. v2.3.1 was published on 2024-09-26, v2.4.0 on 2025-03-05 and v2.5.0 on 2025-07-25. The last push to the repository was on 2026-08-17, so there is recent commit activity behind a version number that has not moved in over a year. That gap is normal for a mature library but worth remembering if you are waiting on a fix: you may be able to take it from git long before a hex release carries it.

One legacy note remains in the docs for very old setups. If you are on Elixir below 1.4 you must also list Bamboo in your `applications`:

```elixir
def application do
  [applications: [:bamboo]]
end
```

On any current Elixir that block is irrelevant, but its presence tells you how long this README has been carried forward.

## Adapters are the extension point, and two of them are for your own machine

An adapter is the set of instructions for talking to one delivery service. Bamboo ships adapters for several popular services including Mandrill, Mailgun and SendGrid, other adapters exist from the community, and you can write your own by implementing the `Bamboo.Adapter` behaviour.

Two adapters exist purely for local work and are the ones you will configure most often. `Bamboo.LocalAdapter` stores emails in memory so they can be viewed during development, and it is declared in the dev environment. `Bamboo.TestAdapter` sends emails to your running process so a test can assert on delivery without anything leaving the machine, declared in the test environment:

```elixir
config :my_app, MyApp.Mailer,
  adapter: Bamboo.LocalAdapter
```

```elixir
config :my_app, MyApp.Mailer,
  adapter: Bamboo.TestAdapter
```

The pattern of per-environment adapters is what makes the environment configuration files the place where mail behaviour is decided. Production gets the real provider, dev gets memory, test gets the process. Nothing about that is Bamboo-specific; the library just makes the adapter the only thing that changes.

The README is direct that other adapter-specific configuration may be required and tells you to read the adapter's own documentation. That is the honest reading: `api_key` is common, but it is not the whole config surface for every provider.

## Hackney options sit one layer below every adapter

Bamboo uses Hackney to make its HTTP requests. If you need to pass options straight through to Hackney, such as controlling timeouts, there is a `hackney_opts` key on the mailer configuration:

```elixir
config :my_app, MyApp.Mailer,
  adapter: Bamboo.MandrillAdapter,
  api_key: "my_api_key",
  hackney_opts: [
    recv_timeout: :timer.minutes(1),
    connect_timeout: :timer.minutes(1)
  ]
```

This is a small feature that removes a common source of confusion. Without it, an email send that times out looks like a Bamboo bug, because the timeout belongs to the HTTP client underneath. With it, you own the failure boundary, and you can raise `recv_timeout` for a provider with a slow first request or lower it so a dead endpoint fails fast.

It also tells you something about the library's shape. The adapter layer is not doing its own socket work, so provider-specific behaviour above HTTP, and transport behaviour below it, are separable. That matters if a future adapter needs a different client entirely, and it matters today if you are migrating an existing app whose Hackney settings live somewhere else.

## deliver_later is a supervisor task, not a queue

This is the section most likely to cause a production surprise, so read it precisely. `deliver_later` exists so that sending email does not block process completion, with the README's example being a web request in Phoenix.

What it actually does by default is use `Bamboo.TaskSupervisorStrategy`, and the README describes that strategy as sending the email right away, but in the background without linking to the calling process, so errors in the mailer do not bring down your app. Read that carefully. It is a supervised task started immediately, not a message placed on a durable queue.

If you want a real queue, the extension point is the `Bamboo.DeliverLaterStrategy` behaviour. The README suggests you could implement a strategy that adds emails to a background processing queue such as exq or toniq. So the honest description is that Bamboo gives you the plug point and leaves the queue to you, which is the right trade-off for a library that does not want to pick your job system, but it does mean `deliver_later` out of the box is not a delivery guarantee.

The bang variants matter alongside this. `deliver_now!` raises on failure, which is what the getting-started example pipes into. Choosing between the bang and non-bang form is where your error handling policy actually lives.

## Phoenix rendering and viewing mail during development

Bamboo integrates with Phoenix out of the box by using Phoenix views and layouts to render email, which is the feature that keeps templates out of string concatenation. The keyword form of `new_email` shown earlier takes an `html_body` and a `text_body` directly, and for anything more structured you compose with pipes instead of building one large keyword list.

For development, the library provides a plug you can add to your router to view sent emails, which is what pairs with the local adapter. The two are complementary rather than alternatives: the local adapter decides that mail is kept rather than sent, and the plug is how you look at what was kept. Set up only one and the feature is half there.

The README also mentions integration test helpers for exercising delivery end to end, separate from the unit test story of asserting on email fields. In practice that distinction is useful: a unit test checks the email fields, while an integration test can verify the adapter against a real service. For a library that talks to third-party HTTP APIs, keeping the second kind is what catches a provider changing its request shape.

## Maintenance, governance and what to check before upgrading

Bamboo is now maintained by the BEAM Community, and the README credits thoughtbot for creating and maintaining it for a long time before that. That handover is the main governance fact: Bamboo is now maintained by BEAM Community, which also credits thoughtbot for creating and maintaining it.

The repository layout is conventional for an Elixir package and tells you where to look. `lib/` holds the source, `test/` the tests, `guides/` long-form documentation, `bin/` helper scripts, `mix.exs` and `mix.lock` the build definition, and `CHANGELOG.md` the release history. Tooling config is committed at the root: `.credo.exs` for static analysis, `.formatter.exs` for `mix format`, `.commitlintrc.yml` for commit message rules, and `.tool-versions` for version pinning of the language toolchain. A `coveralls.json` file is also committed at the repository root alongside the CI workflow configuration.

For an upgrade, the practical note is that the release cadence is slow, so check that your version constraint includes the release you need. Bumping from `~> 2.3.0` style constraints means reading `CHANGELOG.md` for what changed between minor versions rather than tracking a rapid stream, and reading the adapter docs for your provider, since adapter behaviour is where provider APIs move.

## Conclusion

Adopt Bamboo if you run Elixir or Phoenix and want email creation to be ordinary functions you can pipe and assert on, because separating `Bamboo.Email` from `Bamboo.Mailer` is what makes both unit and integration tests cheap. Do not adopt it if you need a delivery queue that is guaranteed durable out of the box, because the default `deliver_later` strategy is `Bamboo.TaskSupervisorStrategy`, which starts sending right away under a supervisor rather than enqueuing durably, and the README points at implementing `Bamboo.DeliverLaterStrategy` yourself or writing a strategy for exq or toniq. Verify first that the adapter you choose is one Bamboo ships, read that adapter's own config keys in its docs, and decide whether your `hackney_opts` timeouts match your provider's slowest endpoint.

## FAQ

### How do I install Bamboo in an Elixir project?

Add `{:bamboo, "~> 2.3.0"}` to the deps list in mix.exs and run `mix deps.get`. You can instead use `{:bamboo, github: "beam-community/bamboo"}` to follow the main branch, in which case the README on that branch may describe changes not yet published to hex.

### How do I test emails in Bamboo without sending them?

Declare `Bamboo.TestAdapter` in your test environment, which sends emails to your running process. In development, `Bamboo.LocalAdapter` stores emails in memory so they can be viewed through the plug Bamboo provides for your router.

### Is deliver_later a background queue in Bamboo?

Not by default. It uses `Bamboo.TaskSupervisorStrategy`, which sends the email right away in the background without linking to the calling process. For a real queue you implement the `Bamboo.DeliverLaterStrategy` behaviour, for example for exq or toniq.

### How do I control HTTP timeouts in Bamboo?

Bamboo uses Hackney, and the `hackney_opts` key on the mailer configuration passes options straight through, including `recv_timeout` and `connect_timeout`.

### Can I add support for a mail provider Bamboo does not ship?

Yes. An adapter is a set of instructions for one delivery service, and you write one by implementing the `Bamboo.Adapter` behaviour, then set it as the `adapter` value in your mailer configuration.

## Sources

- [beam-community/bamboo on GitHub](https://github.com/beam-community/bamboo)
- [License: MIT](https://github.com/beam-community/bamboo/blob/main/LICENSE)
- [Project website](https://hex.pm/packages/bamboo)
- [README](https://github.com/beam-community/bamboo/blob/main/README.md)
- [Releases](https://github.com/beam-community/bamboo/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/beam-community-bamboo
