Library / SDK
vulcand/oxy avatar
vulcand/oxy

vulcand/oxy, the eight Go handlers behind Vulcand's proxy engine

Go middlewares for HTTP servers & proxies

2,103 stars338 forksGoApache-2.0

At a glance

What is it?
vulcand/oxy is a set of eight Go http.Handler middlewares for proxying, balancing, retries and limits, assembled by hand inside your own binary. It hands you the pieces Vulcand's reverse proxy engine is built from, and far less documentation than you would want around them.
Who is it for?
Adopt oxy when you are writing Go, already own an http.Server, and need load balancing, request replay or connection caps inside the same binary, because the chain is short to assemble and every piece stays auditable in your own source. Do not adopt it expecting a configured proxy process, working documentation for five of the eight handlers, or a published upgrade path to follow.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A library of http.Handler values, not a proxy binary

vulcand/oxy is a Go library, not a program, so there is no daemon to start and nothing to install. The top level of the repository is one directory per handler plus internal/, memmetrics/, testutils/ and utils/, alongside a .golangci.yml config, a Makefile and the module files go.mod and go.sum. What you get is a set of values that satisfy the http.Handler interface, and a reverse proxy that exists only once you nest them.

Each value covers one narrow job. Stream passes requests through untouched and supports chunked encoding with a configurable flush interval. Forward sends a request to a remote location and rewrites headers, Roundrobin is a round-robin load balancer, Buffer retries and buffers requests and responses. Connlimit caps simultaneous connections and Ratelimit implements a tokenbucket algorithm. Trace writes structured request and response logs, and the Circuit Breaker handler follows Hystrix style behaviour.

Because every handler takes the http.Handler it wraps, composition order is ordinary Go code compiled into your binary rather than a configuration file parsed at start-up. That is the appeal for a developer who already owns an http.Server and wants proxying, replay and connection caps inside the same process.

The alternative from the same organisation is Vulcand, which uses this library as its reverse proxy engine and exposes the same handlers as part of a proxy program you configure and supervise instead of compiling in. No configuration file example for that program appears here, so choosing it means reading a different repository.

forward.New(false) and the boolean nobody explains

The first listing is the shortest route from nothing to a proxying server, and the awkward details start immediately.

go

import (
  "net/http"
  "github.com/vulcand/oxy/v2/forward"
  "github.com/vulcand/oxy/v2/testutils"
  )

// Forwards incoming requests to whatever location URL points to, adds proper forwarding headers
fwd := forward.New(false)

redirect := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
    // let us forward this request to another server
		req.URL = testutils.ParseURI("http://localhost:63450")
		fwd.ServeHTTP(w, req)
})
	
// that's it! our reverse proxy is ready!
s := &http.Server{
	Addr:           ":8080",
	Handler:        redirect,
}
s.ListenAndServe()

The handler registered on the server is a closure that rewrites req.URL and then passes the request to the forwarding handler, so the upstream can be chosen per request instead of fixed in advance. Two ports appear in nine lines: the listener on :8080 and the upstream on 63450.

Two details reward a close look. The boolean argument to forward.New(false) is never explained anywhere in the README, so what flipping it does has to come from the package documentation on pkg.go.dev. And the opening example imports github.com/vulcand/oxy/v2/testutils, a directory that exists for the project's own tests, then calls testutils.ParseURI to build the target URL. A production service has no reason to carry that import, yet the canonical first example cannot do without it.

roundrobin.New returns two values and one is dropped

With a single upstream, nothing balances. The second listing keeps the same forwarding handler and wraps it in the load balancer:

go

import (
  "net/http"
  "github.com/vulcand/oxy/v2/forward"
  "github.com/vulcand/oxy/v2/roundrobin"
  )

// Forwards incoming requests to whatever location URL points to, adds proper forwarding headers
fwd := forward.New(false)
lb, _ := roundrobin.New(fwd)

lb.UpsertServer(url1)
lb.UpsertServer(url2)

s := &http.Server{
	Addr:           ":8080",
	Handler:        lb,
}
s.ListenAndServe()

The chain changes shape here. In the first example the server held an http.HandlerFunc that picked a target per request; now the server holds lb, and the balancer owns the upstream list, reached through UpsertServer calls. The README does not document whether a repeated call for one address replaces the entry or appends a duplicate, so registration behaviour has to be read from the source before you rely on a config reload path.

Every error in the chain is discarded. `lb, _ := roundrobin.New(fwd)` drops the value the constructor returns, and so do buffer.New and s.ListenAndServe(). At start-up a misconfigured upstream is then indistinguishable from a healthy one, which is the widest gap between a README snippet and the same wiring in production.

buffer.Retry needs a predicate you write yourself

The third listing adds request replay, and it is the only place where the library asks you to write a small expression instead of passing a flag:

go

import (
  "net/http"
  "github.com/vulcand/oxy/v2/forward"
  "github.com/vulcand/oxy/v2/buffer"
  "github.com/vulcand/oxy/v2/roundrobin"
  )

// Forwards incoming requests to whatever location URL points to, adds proper forwarding headers

fwd := forward.New(false)
lb, _ := roundrobin.New(fwd)

// buffer will read the request body and will replay the request again in case if forward returned status
// corresponding to nework error (e.g. Gateway Timeout)
buffer, _ := buffer.New(lb, buffer.Retry(`IsNetworkError() && Attempts() < 2`))

lb.UpsertServer(url1)
lb.UpsertServer(url2)

// that's it! our reverse proxy is ready!
s := &http.Server{
	Addr:           ":8080",
	Handler:        buffer,
}
s.ListenAndServe()

Read the predicate literally. IsNetworkError() decides whether a returned status counts as a transport failure, and Attempts() < 2 bounds the counter, so a request whose body has been read goes out again after a Gateway Timeout and no further. The retry policy is yours to write rather than a toggle, and any status you do not classify as a network error is never retried.

Nothing here says which component evaluates that expression. The module requires github.com/vulcand/predicate v1.3.0, yet no line in the documentation connects that dependency to the retry string, so treat the function names inside it as an API to check rather than assume.

Two smaller things sit in the same listing. The handler variable is named buffer, the same identifier as the package that created it, so later code in that file cannot reach buffer.Retry without shadowing. And the comment inside the block misspells network, a detail that only surfaces when you read the snippet closely.

Stream, Connlimit, Ratelimit, Trace and cbreaker get one line

Five of the eight handlers arrive as a single sentence each, with no listing at all. Stream passes requests through and supports chunked encoding with a configurable flush interval. Connlimit is a simultaneous connections limiter, Ratelimit is a rate limiter based on a tokenbucket algorithm, Trace is a structured request and response logger, and the Circuit Breaker handler is Hystrix-style.

That imbalance is the practical constraint for anyone sizing this library. Forward, Roundrobin and Buffer come with runnable Go you can copy; the other five arrive as a name, a package path and a promise, with behaviour available only from the source or the package pages on pkg.go.dev. Stream is the clearest case, since the flush interval is its only documented knob and the README gives neither the constructor call nor a default value for it.

So the useful question is which of these you actually need. Cap concurrency per client and connlimit is a single wrapper. Stream chunked output with prompt delivery and the flush interval decides when bytes leave. Want fault isolation from a failing upstream and the Hystrix-style line gives you an algorithm family with no threshold, window or half-open setting, which is exactly the number a circuit breaker cannot run without.

No install command, no benchmark, no published coverage

The README has no installation step. It never shows the command that adds the module to a build, never pins a version, and never prints a go.mod fragment, so the /v2 suffix carried by the import paths is the only clue that the module path is github.com/vulcand/oxy/v2. That is enough for an experienced Go developer, but it does make the first hour of adoption partly improvised.

No benchmark appears anywhere in the repository, and no coverage figure is published either. The status section claims the library is covered by tests, and the Makefile makes that checkable locally with `go test -race -cover -count 1 ./...`, which prints a coverage number on your own machine since the project publishes none. That target runs clean first, and clean deletes every file matching flymake_* under the tree before removing cover.out, a pattern the README never explains. The default target chains clean, checks and test, so a full local run means golangci-lint run followed by the race suite.

Two build facts matter more than the rest. go.mod sets go 1.25.0, the toolchain floor any consumer inherits, and the linter is not a module dependency, so golangci-lint has to be present on the machine before the checks target does anything.

Seven direct dependencies and five indirect ones

Dependency surface is the other half of an adoption decision, and go.mod is unusually explicit about it. Seven modules sit in the direct require block: github.com/HdrHistogram/hdrhistogram-go v1.3.0, github.com/gorilla/websocket v1.5.3, github.com/mailgun/multibuf v0.2.0, github.com/segmentio/fasthash v1.0.3, github.com/stretchr/testify v1.11.1, github.com/vulcand/predicate v1.3.0 and golang.org/x/net v0.57.0. Five more appear as indirect, among them gopkg.in/yaml.v3 v3.0.1.

Two entries deserve a second look. testify v1.11.1 is a test-only library declared as a direct requirement, which tells you the project's own suite is part of what you inherit on import. And golang.org/x/net v0.57.0 is the line that tracks the Go toolchain cadence, so it is the one to expect to move in your build over time.

The repository is not archived, and the last push was on 2026-07-14. The module is Apache-2.0 and the repository has no GitHub releases, so there is no tagged version to pin and no upgrade note to read first; the go.mod file is the only upgrade record on offer.

Two of those dependencies have no stated purpose in the documentation. Nothing connects github.com/gorilla/websocket v1.5.3 or github.com/segmentio/fasthash v1.0.3 to a named handler, which matters if you audit what a proxy middleware pulls into your binary.

memmetrics and utils ship without a line of documentation

Compare the tree with the handler list one more time. The README names eight handlers, and the repository has twelve top-level directories: one per handler, plus internal/, memmetrics/, testutils/ and utils/. Three of those four are unexplained anywhere in the documentation, and go.mod requires github.com/HdrHistogram/hdrhistogram-go v1.3.0 without a single line tying that dependency to a handler. memmetrics is a bare directory name and nothing more.

The listings have their own gap. In the second and third snippets, lb.UpsertServer(url1) and lb.UpsertServer(url2) are called on identifiers that are never declared, and no snippet carries a package clause or a main function, so none of the three compiles as printed. They are fragments to drop into a file you write, which is fine, but it leaves the registration API for an additional upstream entirely to you.

If you want a working proxy without writing Go, the documented path is Vulcand, the same organisation's program that uses these handlers as its reverse proxy engine: a configured process to supervise rather than a library to embed. If you want one upstream and nothing more, the ReverseProxy type in net/http/httputil is the shorter route, and oxy earns its place through the other seven handlers rather than through proxying itself.

Editorial conclusion

Adopt oxy when you are writing Go, already own an http.Server, and need load balancing, request replay or connection caps inside the same binary, because the chain is short to assemble and every piece stays auditable in your own source. Do not adopt it expecting a configured proxy process, working documentation for five of the eight handlers, or a published upgrade path to follow. Before committing, run the race suite the Makefile defines and read memmetrics/, which no line of the documentation ever describes.

Frequently asked questions

How do I add vulcand/oxy to a Go project?

The README shows no dependency command and no go.mod fragment. The import paths in its examples carry the /v2 suffix, which tells you the module path is github.com/vulcand/oxy/v2, and go.mod declares go 1.25.0 as the toolchain floor.

How do I make vulcand/oxy retry a failed request?

Wrap the next handler with buffer.New and pass buffer.Retry with a predicate expression, for example `IsNetworkError() && Attempts() < 2`, which replays a buffered request once a forward returned a status that looks like a network error. The README does not name the component that evaluates that string.

Is vulcand/oxy maintained and how is it licensed?

The repository is not archived and the last push was on 2026-07-14. The module is Apache-2.0, and the repository has no GitHub releases, so no tagged version exists to pin.

Does vulcand/oxy ship a proxy I can run without writing code?

No. It is a library of http.Handler values and the README gives no binary, service unit or configuration file. The reverse proxy program built on these handlers is Vulcand, a separate project.

How do I cap connections or rate limit with vulcand/oxy?

The connlimit handler is a simultaneous connections limiter and the ratelimit handler uses a tokenbucket algorithm, and both take the next http.Handler the same way forward and roundrobin do. The README gives each of them one line, with no constructor call and no configuration key.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. vulcand/oxy on GitHub
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/vulcand-oxy.svg)](https://hysenlabs.com/projects/vulcand-oxy)

Was this useful?