A real-world story of making a GoFr API go from “fine” to “feels instant” using observability, DB tuning & service-level reliability.
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
}
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 = ?
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
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"},
)
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
}
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)