DEV Community

Umang Mundhra
Umang Mundhra

Posted on

🚀 From “Works Fine” to “Feels Instant”: Tuning a GoFr API Like Engineers, Not Magicians

A real-world story of making a GoFr API go from “fine” to “feels instant” using observability, DB tuning & service-level reliability.

From “Works Fine” to “Feels Instant”: Tuning a GoFr API Like Engineers, Not Magicians

Most performance stories begin dramatically — with outages, angry PMs, dashboards on fire.

Ours began mundanely.

The API was fine. It returned order details, fetched user info, stitched both, responded in a couple hundred ms. P50 looked good. No error spikes. Monitoring wasn’t screaming.

But the UI told a different story.

Click → …loading… → click → …loading again…

Not broken.
Not slow.
Just not delightful.

So we decided to see how far we could push fine toward fast — without rewriting the architecture, adding new libraries, or making the code unpleasant to work with.

This is how GoFr made that journey almost embarrassingly smooth.


🧩 The Starting Point

A simple GoFr handler:

func GetOrderDetails(ctx *gofr.Context) (any, error) {
    orderID := ctx.Param("id")

    // DB call
    var order Order
    err := ctx.SQL.QueryRowContext(ctx,
        "SELECT id, user_id, status, total FROM orders WHERE id=?", orderID,
    ).Scan(&order.ID, &order.UserID, &order.Status, &order.Total)
    if err != nil {
        return nil, err
    }

    // HTTP call
    userService := ctx.GetHTTPService("user-service")
    resp, err := userService.Get(ctx, "/users/"+order.UserID, nil)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var user User
    json.NewDecoder(resp.Body).Decode(&user)

    return OrderDetailsResponse{Order: order, User: user}, nil
}
Enter fullscreen mode Exit fullscreen mode

Clean. Predictable. One context.
Nothing to rewrite — we just needed to understand what was eating time.


🔍 Step 1 — Let Observability Tell the Story

No custom tracing, no slogging through logs, no extra plugins.
GoFr already ships with:

✔ Tracing per request
✔ SQL + HTTP span instrumentation
✔ Structured logs linked to traces

Within minutes, patterns emerged:

⛔ occasional DB spans ballooned
🟢 HTTP calls were steady
📈 latency spikes mapped to DB wait time

Mystery solved.
No guessing → just graphs.


⚡ Step 2 — Fixing the Real Culprit (SQL)

The query looked harmless:

SELECT id, user_id, status, total FROM orders WHERE id = ?
Enter fullscreen mode Exit fullscreen mode

But on a growing table, “harmless” becomes “sluggish”.

We added the correct index + tuned connection pooling:

DB_MAX_OPEN_CONNS=25
DB_MAX_IDLE_CONNS=10
DB_CONN_MAX_LIFETIME=5m
Enter fullscreen mode Exit fullscreen mode

No code change.

Spans shrank immediately. P95 dipped.
The app felt faster.


Step 3: Hardening the HTTP Call — The Right Way in GoFr

The downstream user service wasn’t slow, but it was still a point of risk:

  • if it slowed down → our API slowed down
  • if it failed → we returned errors upstream

Instead of wrapping custom retry logic manually, GoFr lets us configure reliability at the service layer itself — once, globally.

We updated the service registration like this:

a.AddHTTPService("user-service", "https://users.internal",
    &service.RetryConfig{MaxRetries: 3},
    &service.CircuitBreakerConfig{
        Threshold: 5,
        Interval:  2 * time.Second,
    },
    &service.RateLimiterConfig{
        Requests: 50,
        Window:   time.Second,
        Burst:    75,
    },
    &service.HealthConfig{HealthEndpoint: "health"},
)
Enter fullscreen mode Exit fullscreen mode

Now every request to user-service automatically benefits from:

✔ retry handling
✔ backoff behavior
✔ rate limiting
✔ circuit breaker protection
✔ unified tracing + logging through ctx

No wrappers, no middleware sprawl, no rewriting handlers.

The handler stayed as clean as before:

func GetOrderDetails(ctx *gofr.Context) (any, error) {
    orderID := ctx.Param("id")

    // fetch order (unchanged)
    ...

    userService := ctx.GetHTTPService("user-service")

    resp, err := userService.Get(ctx, "/users/"+order.UserID, nil)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var user User
    json.NewDecoder(resp.Body).Decode(&user)

    return OrderDetailsResponse{Order: order, User: user}, nil
}

Enter fullscreen mode Exit fullscreen mode

This step didn’t drastically reduce average response time —
but it made latency spikes vanish and failures behave predictably.

That’s the difference between being fast and being reliable.


🪶 Step 4 — Send Only What UI Needs

We removed unused fields from the response payload.

Smaller payload = faster in real networks.
Performance is rarely one giant win — it’s a hundred tiny wins.


🎯 The Most Interesting Part?

Through all improvements, our handler never got messy.

func GetOrderDetails(ctx *gofr.Context) (any, error) { ... }

One context for everything:

🟦 DB
🟦 HTTP
🟦 Logs
🟦 Tracing
🟦 Metrics

No glue code. No noise. Only logic.

That’s the real win.


💭 What We Learned
Observability first → performance second
Small DB + HTTP improvements beat fancy rewrites
A unified context keeps code elegant under load
Fast is good. Predictably fast is better.
GoFr didn’t give us magic.
It gave us clarity — and clarity made us faster.


If Your API is “Fine” Today…

Before optimizing everything:

👉 trace it
👉 index what matters
👉 configure retries
👉 fix first bottleneck
👉 keep code simple

You might be one index + one retry away from delight.


📚 Try this yourself

🚀 Quick start: https://gofr.dev/docs/quick-start
💻 Source code: https://github.com/gofr-dev/gofr

Sometimes the best upgrade isn’t changing tech —
It’s unlocking the tech you already have.

Top comments (0)