Skip to content

A little blurb about Halcyon

Or my attempt at un-brain-damaging Traefik

AngryDutchman
Sep 7, 20266 min read

o I've been using Traefik for quite a while (late 1.0 stages) at work, and while I love the concepts behind it, I've never been a big fan of how it actually "Did The Thing(tm)".

I've been using it to run my own projects for a while too and have gotten way too frustrated with it, so as a result of the (recent) yes-AI no-AI discourse I decided to just write a spec and let Claude go ape on it for a while, just to see what would roll out. And no, it's not vibe coded. It was given a tight spec, I reviewed what it did, gave Claude corrections, which doesn't make it vibed, in my opinion.

This is also the time where I'm going to give you a very big disclaimer: Halcyon is by and large just me building something that works for me - whether it works for anyone else? I have no idea. I just wanted something that would "Do What I Want(tm)" the way I wanted it.

Anyway, you can look at the code here - be careful, here be dragons.

So how do Traefik and Halcyon compare? Let me tell you the ways in which I think Traefik is braindead, and how Halcyon solves it!

Bloat!

Traefik has a stupendous list of features and options that I frankly never use. I don't need 10 different service discovery mechanisms (Nomad will do, thank you), I don't need 6 different KV stores it can read it's configuration from when the things I need to be dynamic can't be.

Halcyon has very little bloat. In my opinion. It basically routes stuff from point A to point B. And maybe a few little things on top.

Routing

Traefik's routing is painful, I don't want to go too deep into the details, but routing an entire site, but wanting to exclude a single path from having an option applied requires defining 2 routers on a service. This gets annoying, quick.

Halcyon takes a more nginx-ish approach; you just define halcyon.server.domain=foo.com on a service and there you go, it's routed. You can route multiple domains too, and even wildcards: halcyon.server.domain=foo.com *.foo.com bar.com flibberdygibbet.site.org - and presto majesto, routing multiple hostnames and wildcards. Easy.

And just like nginx, you can define a location. A location can have options applied to it that alter how the request is handled; for example, if I have a metrics endpoint that I want to totally not route because it's for internal network consumption only? Two tags. halcyon.location.metrics.path=/metrics and halcyon.location.metrics.exclude and it's now never ever ever routed.

Other things it can do at the "server" and location level is modify some buffering options, enable authentication, and so on. RTFineM ;)

Authentication

Traefik has the notion of "forward auth" - basically it'll call another service to determine if you get to see something or not. Nginx has a similar system. Except in Traefik it gets a bit tricky because if that service is down? So is everything that uses it.

So I just figured okay, what the hell, ATproto amirite? So it has built-in ATproto oauth. You can basically define it at the server or location level; all it does is allow someone to sign in, and if they are on the allow list they get a session cookie and that's it. Doesn't really do much beyond that, but... beats having yet-another-external-service.

Rate limiting

Traefik has a built-in rate limiter that pretty much forces you into using Redis or Valkey - it's nice, but it's either a lot of service tags to set up, or a lot of config file work. Halcyon does it different, and it's turned into a per-request hook. It'll just fire a quick request at another service (no, no, totally ~different~ than forward-auth in Traefik I promise... not really but I'm just hypocritical like that) and that just returns a status code and some headers that determine if someone gets to, you know, see the goodies.

Quota enforcement

Of sorts. This mainly based on a project I'm building where I eventually need to block uploads when people exceed their storage quota. Okay so it's for a PDS. Which doesn't like, support any form of quota control. So, hook to the rescue. It also calls an external service, and that service just returns the # of bytes that are still allowed to be uploaded. Go past it? Upload rejected.

Buffering

Traefik doesn't generally buffer and just streams things to the backend. Great, but sometimes you do want to buffer things, or play some games with transfer encodings. So it's got some options for that. Traefik can do buffering, but again, middleware definitions...

Service discovery

If you use Consul, or etcd, or one of the other myriad service discovery tools, Traefik's got you covered. Halcyon? It knows only Nomad. That's all it looks at.

Configuration

Traefik has a static configuration, and dynamic configuration. It can read dynamic configuration from a variety of KV backends, and also from files on disk. Well, one file on disk. Or one directory. That it watches for changes. This got simplified; there is exactly 1 file Halcyon will watch, and that is the file with static server/location definitions that you really only use if you need to route something that isn't a Nomad service.

TLS certificates

The big fat gorilla in the room. Traefik has a built-in Let's Encrypt certificate issuer, which will use HTTP-01 challenges. That means you can't issue wildcard certificates with it, and you very quickly end up with an enormous pile of certificates, all stored in a single JSON file.

Now run 3 instances of Traefik. Now you have 3 JSON files. And 3 requests to Let's Encrypt. And now suddenly your rate limits get hit. Or one node's JSON file goes kerpoof and now it needs to re-issue everything. You can solve that, but you'll have to sign up for their enterprisey maze-of-tunnels Traefik Hub (or w/e it's called thes days) thing so you can... gasp have multi-instance Let's Encrypt certificate issuance and keep things synchronized.

Halcyon does this very different; it uses Raft, not only to elect a leader, but also as the source of truth for certificates. The leader election is done to ensure that only a single node will ever talk to Let's Encrypt, and can make sure rate limits don't get exceeded. Issued certificates are stored in Raft, and replicated to all followers. And presto, instant certificate availability (well, give it ~500ms). Added benefit is that even if a node goes down, "shit just works(tm)" and there's never going to be a thundering herd "re-issue all the things" problem. Unless of course you zap a bunch of the raft databases, but then it's on you.

tl:dr;

Basically built this to get similar functionality on a smaller footprint with fewer bells and whistles. Just a few chimes, really, that I wanted.

Again, standard disclaimer applies: it works for me, because I built it, for me. If you want to use it, feel free, but if it breaks you get to keep all the pieces. I've done a brief security audit on it, and it seems alright, but there's absolutely no guarantees Halcyon can't be used to pwn your shit. Danger, Will Robinson, danger.

Did you enjoy this article?

Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.

Across the AtmosphereDiscussions