Skip to content

Fiber Zero Allocation Caveat: Save Your Sanity

Fiber is a fast HTTP framework for Go, inspired by Express.js. One of its performance features is zero allocation, which optimises for high throughput by reusing memory across requests. While this sounds great on paper, it creates subtle bugs that can waste hours of debugging time.

The Zero Allocation Feature

By default, Fiber values returned from fiber.Ctx are not immutable. They are optimized for performance by reusing the same underlying memory across requests. This means:

func handler(c *fiber.Ctx) error {
    name := c.Params("name")
    // 'name' points to memory that will be reused
    // for the next request
    return c.SendString("Hello " + name)
}

This works fine if you only use the value within the handler. But if you store the value, pass it to another goroutine, or use it with libraries that expect immutable strings, you'll encounter confusing bugs.

The Problem with Prometheus

A common scenario where this bites developers is when using the Prometheus client library. Consider this seemingly innocent code:

import (
    "github.com/gofiber/fiber/v2"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
)

var requestCounter = promauto.NewCounterVec(
    prometheus.CounterOpts{
        Name: "http_requests_total",
        Help: "Total number of HTTP requests",
    },
    []string{"method", "path"},
)

func handler(c *fiber.Ctx) error {
    method := c.Method()
    path := c.Path()

    // This looks correct but will cause issues
    requestCounter.WithLabelValues(method, path).Inc()

    return c.SendString("OK")
}

The Prometheus client expects label values to remain stable. However, Fiber's zero allocation means the underlying bytes of method and path get reused for subsequent requests. This leads to bizarre and hard-to-debug issues.

Real-World Example

In prometheus/client_golang#1429, developers reported cryptic errors like:

collected metric "ns_http_nnnnnn" { label:{name:"ID" value:"171"} counter:{value:1}}
was collected before with the same name and label values

The metric was only registered once, yet Prometheus complained about duplicate registration. The issue was baffling because the code looked correct, and the error appeared inconsistently under load.

Even more bizarre was a case where a label value tenant-id mysteriously became senant-id (the 't' changed to 's'). This happened because the underlying memory was being reused for a different request, and partial data from the new request overwrote the old label value while it was still referenced by Prometheus.

Why This Happens

Fiber returns strings that point to a reusable buffer. Prometheus stores references to these strings. When the next request arrives, Fiber overwrites the buffer, but Prometheus still holds the old reference - same hash key, different data, inconsistent state. One developer spent an entire day debugging this before discovering the root cause.

The Solution: Disable Zero Allocation

The fix is straightforward but not obvious. Add Immutable: true to your Fiber configuration:

app := fiber.New(fiber.Config{
    Immutable: true,
})

This makes all context values immutable by allocating new memory for each value. The performance impact is negligible for most applications, but it eliminates an entire class of subtle bugs.

Alternative Approaches

If you absolutely need the zero allocation optimisation, you can manually copy strings using Fiber's utility:

import "github.com/gofiber/fiber/v2/utils"

func handler(c *fiber.Ctx) error {
    method := utils.CopyString(c.Method())
    path := utils.CopyString(c.Path())

    requestCounter.WithLabelValues(method, path).Inc()
    return c.SendString("OK")
}

This approach works, but it's error-prone - you need to remember to copy every value that might be stored or used outside the request handler.

My Recommendation

Unless you've profiled your application and confirmed that allocations are a bottleneck, set Immutable: true from day one. The performance difference is rarely noticeable in real-world applications, but the time saved debugging mysterious issues is significant.

The zero allocation feature is a premature optimisation for most use cases. Modern Go's garbage collector is efficient enough that a few extra allocations won't hurt your performance. What will hurt is spending hours debugging why your metrics are corrupted or why your application panics under load.

Save your sanity. Add this to your Fiber configuration:

app := fiber.New(fiber.Config{
    Immutable: true,
})

Your future self will thank you.

References