Once more than a couple of services in your system send email, someone asks the obvious question: shouldn’t we standardize this? Two designs usually come up. One is a sidecar: a small process deployed next to each service instance that the service talks to over localhost. The other is a centralized email service: one internal service that every application calls to send mail.

They’re often framed as alternatives. They aren’t, quite. A sidecar is good at standardizing how services send email, but on its own it’s usually worse than a centralized service at standardizing what happens to that email. The strongest design tends to combine the two.

Where a sidecar helps

The appeal of a sidecar is a single, consistent contract. Every service talks to localhost using the same interface, either SMTP or a small HTTP API, no matter what language it’s written in or which team owns it. Behind that contract, the sidecar can take care of the work each team would otherwise hand-roll:

  • Retries and local buffering, so a brief provider or network hiccup doesn’t surface as a failed request in the app.
  • Templating, so message layout and branding come from one place.
  • Logging and tracing, emitted the same way from every service.
  • Authentication to the backend, so application code never touches provider credentials directly.

All of that lives in one shared container image instead of a dozen libraries that drift apart over time. That’s the Don’t Repeat Yourself principle applied at the infrastructure level: the logic exists once and is distributed everywhere.

A sidecar also avoids forcing every team onto the same language SDK. A Python service, a .NET service, and a legacy Java app can all speak SMTP to localhost. And because the sidecar sits in the service’s own pod, an outage in some central system doesn’t have to block the app’s request path: the sidecar accepts the message and deals with delivery later.

Where a sidecar falls short

The trouble is that many of the most important email concerns are inherently global. They depend on what every service is doing, not what one instance is doing, and a per-instance process can’t own them without a shared backend anyway:

  • Suppression and bounce lists, unsubscribes, and consent. If a recipient hard-bounces or unsubscribes after a message from the billing service, the marketing service needs to know before it sends the next one. That list has to live somewhere shared.
  • Org-wide rate limiting and throttling. Mailbox providers judge your sending domain, not your individual pods. Protecting domain reputation means limiting total volume across the fleet, which no single sidecar can see.
  • Provider failover, DKIM, and sending-domain management. Deciding when to fail over from one email provider to another, and keeping DKIM keys and domain configuration consistent, are organization-level decisions. Spreading them across every pod means spreading them across every deployment.
  • Cross-service deduplication and a single audit trail. When compliance or security asks “what did we send to this person, and why?”, the answer shouldn’t require stitching together logs from every sidecar in the cluster.

In each case, a sidecar ends up as a client of some central store or service. At that point you have a centralized email service whether you planned for one or not; it’s just not designed as one.

The fleet costs of sidecars

Sidecars also bring operational costs that grow with the size of your fleet:

  • Upgrades and version drift. Every change to the sidecar is a rollout across every service. In practice, some services lag behind, and you live with several versions in production at once.
  • Credential sprawl. If the sidecar authenticates to an email provider, those credentials are distributed to every pod that runs it. That’s the opposite of what a security team wants for keys that can send mail as your domain, a risk we covered in App-Sent Emails Use Your Domain.
  • Resource overhead. Each instance adds its own CPU and memory, multiplied by every replica of every service.
  • Poor fit outside containers. The pattern assumes a pod or similar co-located runtime. Serverless functions and legacy VMs don’t have a natural place to put a sidecar.

A centralized service on its own has trade-offs too

A centralized email service is the natural owner of the global concerns above. One service holds provider credentials, enforces policy, maintains suppression lists, and records every message. That makes it a single point of enforcement for email policy.

Its main weakness is coupling. If every service calls the central service synchronously in its request path, the central service’s availability becomes every service’s availability. And without some standard way to reach it, each team still writes its own client code, with its own retry logic and its own mistakes.

What we recommend: central owner, thin local edge

Combine the two, with each doing the job it’s suited for:

  1. Keep a centralized email service as the single owner of policy, providers, suppression, and audit. It’s the only system that holds provider credentials and the only place where organization-wide rules are enforced.
  2. Feed it asynchronously through a queue, so callers aren’t coupled to its availability. A service that needs to send a password reset hands off the message and moves on; the central service delivers it when it can.
  3. Give each service a thin client, either a small library or a thin sidecar, that implements the standard contract: how to format a message, how to hand it off, how to retry the handoff, and how to emit consistent telemetry.

In that setup, the sidecar is a distribution mechanism for standardization, not a replacement for the central capabilities. It’s separation of concerns applied across the system: the local edge handles getting the message out of the service reliably, and the central service decides whether, how, and through which provider it’s delivered.

Which local edge to choose depends on your platform:

  • If you already run a service mesh or a Dapr-style platform, the sidecar slot is cheap. You already deploy, upgrade, and monitor sidecars, so one more fits naturally.
  • Otherwise, a client library plus a shared convention is often simpler. For a .NET shop, that might be a NuGet package that every service references. You get the same consistent contract without adding a new runtime component to every deployment, and it works in serverless functions and on VMs too.

Either way, keep the local piece thin. The more policy it carries, the more you’re back to distributing global decisions across every instance.

Where OutboxServer fits

OutboxServer is built to be the central half of this design. It’s a self-hosted outgoing email service your applications call instead of an email provider. It holds the provider credentials so applications never do, enforces allowed senders, per-application rate limits, and recipient rules, and records which application sent what, to whom, and whether it was delivered, bounced, or flagged as spam.

Because it accepts mail from applications over your internal network, whatever standard local contract you choose, a client library or a sidecar, has one well-defined place to send to.

Bottom line

A sidecar is a good way to make every service send email the same way. It’s a poor place to decide what your organization’s email policy is. Put policy, providers, suppression, and audit in one central service, connect to it asynchronously, and use a thin library or sidecar to make reaching it consistent and easy.