High-Load Go Backend with Echo Framework
In production, a Go server must handle tens of thousands of concurrent connections, process requests in milliseconds, and gracefully shut down during restarts. The standard net/http package lacks built-in rate limiting, graceful shutdown, and flexible routing. That's why we choose Echo – a framework that doesn't impose architecture but provides all the tools for a production-ready solution. Our team has over 7 years of Go development experience and has delivered 50+ projects, from simple APIs to real-time platforms. As stated in the Echo documentation: Echo is a high performance, extensible, minimalist Go web framework.
Why Echo Over Gin?
Echo and Gin solve the same problem, but Echo emphasizes extensibility: middleware, context, binder – all are interfaces that can be replaced. Gin is slightly faster in synthetic benchmarks (e.g., 10k RPS vs 9.5k RPS), but Echo is more architecturally flexible, especially for writing middleware. In real projects, performance differences are negligible – the bottleneck is almost always the database, not the router.
Core Components of an Echo Backend
Initialization and Routing
// internal/server/server.go
package server
import (
"net/http"
"os"
"os/signal"
"syscall"
"context"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/myapp/internal/domain/product"
"github.com/myapp/internal/domain/auth"
custmw "github.com/myapp/internal/middleware"
)
type Server struct {
echo *echo.Echo
product *product.Handler
auth *auth.Handler
logger Logger
cfg Config
}
func New(deps Dependencies) *Server {
e := echo.New()
e.HideBanner = true
e.Validator = custmw.NewValidator()
// Built-in middleware
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
LogMethod: true, LogURI: true, LogStatus: true, LogLatency: true,
LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
deps.Logger.Info("request",
"method", v.Method, "uri", v.URI,
"status", v.Status, "latency", v.Latency)
return nil
},
}))
e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
LogErrorFunc: func(c echo.Context, err error, stack []byte) error {
deps.Logger.Error("panic recovered", "error", err)
return nil
},
}))
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: deps.Config.AllowedOrigins,
AllowCredentials: true,
}))
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(100)))
s := &Server{echo: e, product: product.NewHandler(deps), auth: auth.NewHandler(deps), logger: deps.Logger, cfg: deps.Config}
s.registerRoutes()
return s
}
func (s *Server) registerRoutes() {
api := s.echo.Group("/api/v1")
// Public
authGroup := api.Group("/auth")
authGroup.POST("/login", s.auth.Login)
authGroup.POST("/refresh", s.auth.Refresh)
// Protected
restricted := api.Group("", custmw.JWT(s.cfg.JWTSecret))
restricted.GET("/profile", s.auth.Profile)
products := api.Group("/products")
products.GET("", s.product.List)
products.GET("/:id", s.product.Get)
adminProducts := products.Group("", custmw.JWT(s.cfg.JWTSecret), custmw.RequireRole("admin"))
adminProducts.POST("", s.product.Create)
adminProducts.PUT("/:id", s.product.Update)
adminProducts.DELETE("/:id", s.product.Delete)
}
func (s *Server) Start(addr string) error {
go func() {
if err := s.echo.Start(addr); err != http.ErrServerClosed {
s.logger.Error("server error", "err", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return s.echo.Shutdown(ctx)
}
Custom Validator
// internal/middleware/validator.go
package middleware
import (
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
"net/http"
)
type CustomValidator struct {
v *validator.Validate
}
func NewValidator() *CustomValidator {
v := validator.New()
// Custom tag for slug
v.RegisterValidation("slug", func(fl validator.FieldLevel) bool {
val := fl.Field().String()
for _, c := range val {
if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') {
return false
}
}
return len(val) > 0
})
return &CustomValidator{v: v}
}
func (cv *CustomValidator) Validate(i interface{}) error {
if err := cv.v.Struct(i); err != nil {
errs := err.(validator.ValidationErrors)
fields := make(map[string]string, len(errs))
for _, e := range errs {
fields[e.Field()] = e.Tag()
}
return echo.NewHTTPError(http.StatusUnprocessableEntity, fields)
}
return nil
}
Handler with Echo Context
// internal/domain/product/handler.go
package product
import (
"net/http"
"strconv"
"github.com/labstack/echo/v4"
)
type Handler struct {
svc *Service
}
type CreateRequest struct {
Name string `json:"name" validate:"required,min=2,max=255"`
Price float64 `json:"price" validate:"required,gt=0"`
CategoryID *int `json:"category_id" validate:"omitempty,gt=0"`
Tags []string `json:"tags" validate:"omitempty,max=10,dive,min=1,max=50"`
}
func (h *Handler) Create(c echo.Context) error {
var req CreateRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if err := c.Validate(&req); err != nil {
return err // echo.HTTPError with fields
}
userID := c.Get("userID").(int)
product, err := h.svc.Create(c.Request().Context(), req, userID)
if err != nil {
return mapServiceError(err)
}
return c.JSON(http.StatusCreated, product)
}
func (h *Handler) Get(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid id")
}
product, err := h.svc.GetByID(c.Request().Context(), id)
if err != nil {
return mapServiceError(err)
}
return c.JSON(http.StatusOK, product)
}
WebSocket and SSE
// WebSocket
func (h *NotificationHandler) Subscribe(c echo.Context) error {
userID := c.Get("userID").(int)
conn, _, _, err := ws.UpgradeHTTP(c.Request(), c.Response())
if err != nil {
return err
}
defer conn.Close()
ch := h.broker.Subscribe(userID)
defer h.broker.Unsubscribe(userID, ch)
for msg := range ch {
if err := wsutil.WriteServerMessage(conn, ws.OpText, msg); err != nil {
break
}
}
return nil
}
// Server-Sent Events
func (h *EventHandler) Stream(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentType, "text/event-stream")
c.Response().Header().Set("Cache-Control", "no-cache")
c.Response().Header().Set("Connection", "keep-alive")
c.Response().WriteHeader(http.StatusOK)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-c.Request().Context().Done():
return nil
case t := <-ticker.C:
fmt.Fprintf(c.Response(), "data: %s\n\n", t.Format(time.RFC3339))
c.Response().Flush()
}
}
}
Caching with Redis
// internal/cache/redis.go
type Cache struct {
client *redis.Client
}
func (c *Cache) GetOrSet(ctx context.Context, key string, ttl time.Duration, fn func() (interface{}, error)) (interface{}, error) {
cached, err := c.client.Get(ctx, key).Bytes()
if err == nil {
var result interface{}
if err := json.Unmarshal(cached, &result); err == nil {
return result, nil
}
}
value, err := fn()
if err != nil {
return nil, err
}
if data, err := json.Marshal(value); err == nil {
c.client.Set(ctx, key, data, ttl)
}
return value, nil
}
// Usage in handler
func (h *Handler) List(c echo.Context) error {
cacheKey := fmt.Sprintf("products:list:%s", c.QueryString())
result, err := h.cache.GetOrSet(c.Request().Context(), cacheKey, 5*time.Minute, func() (interface{}, error) {
return h.svc.List(c.Request().Context(), parseListParams(c))
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError)
}
return c.JSON(http.StatusOK, result)
}
How Echo Handles High Load
Echo's mechanisms – allocation-free middleware, context pooling, and built-in rate limiter – deliver low TTFB (e.g., <50ms at 10k RPS) and predictable memory usage. Combined with Redis caching, we keep LCP in the green zone of Core Web Vitals even at 10k RPS. The GetOrSet pattern above is the cache-aside approach we apply to every GET endpoint.
Case study: On a recent e-commerce project, we reduced average response time from 1.2 seconds to 180ms by implementing Redis cache-aside and optimizing SQL queries. The system now handles 10k RPS while maintaining consistent LCP under 2.5 seconds. Infrastructure costs decreased by 40% due to reduced server load.
Common Pitfalls and Solutions
- N+1 queries: Avoid by using Repository pattern with explicit JOINs and pagination. We always use eager loading and single-query approaches.
- Missing graceful shutdown: Implement
echo.Shutdownwith a 10-second timeout to drain connections on restart. - Weak validation: Register a custom validator with business rules (e.g., slug format) to get structured error responses.
- CORS misconfiguration: Set AllowOrigins precisely in CORS middleware to prevent broad origins.
What's Included in Our Work (Deliverables)
Our Go backend development service includes:
- Architecture design: pattern selection (Repository, BFF), DB schema, API contracts (OpenAPI 3.0)
- Full implementation: middleware, handlers, services, integration tests (80%+ coverage)
- Infrastructure setup: Docker, docker-compose, CI/CD (GitHub Actions), Nginx config
- Performance optimization: profiling, caching strategy, load testing (k6)
- Documentation: OpenAPI (Swagger), README with run instructions, deployment guide
- Access: full source code, commit history, credentials, admin panel
- Training: optional 1-day team session on running and extending the solution
- Warranty: 2 weeks post-delivery support for bug fixes and questions
How We Work: Process and Scope
| Component | Description |
|---|---|
| Architecture | Pattern selection (Repository, BFF), DB schema, API contracts |
| Development | Middleware, handlers, services, integration tests |
| Infrastructure | Docker, docker-compose, CI/CD (GitHub Actions) |
| Documentation | OpenAPI (Swagger), README with run instructions |
| Deployment | Nginx setup, certificates, monitoring (Prometheus/Grafana) |
| Warranty | 2 weeks post-delivery support, bug fixes per contract |
- Analysis – We audit the current architecture, profile bottlenecks.
- Design – Schemas, stack choices (PostgreSQL/Redis/Elasticsearch).
- Implementation – Write code, cover with unit and e2e tests.
- Testing – Load tests (vegeta/k6), check LCP/CLS/INP.
- Deployment – Deploy on your hosting (Selectel, Beget, REG.RU) or cloud.
- Handover – Full documentation, access, optional team training.
Timeline and Budget Estimates
- Scaffold + DI + database: 3–5 days
- Routes, handlers, middleware: 1–1.5 weeks
- Services + repositories: 1–3 weeks
- WebSocket/SSE: 3–5 days extra
- Tests + Docker: 1 week
An API for a typical site takes 4 to 8 weeks. Budget ranges from $15,000 to $40,000 depending on complexity (e.g., real-time features, number of integrations). Proper caching can reduce infrastructure costs substantially – we've seen up to 40% monthly savings for clients. For a detailed estimate, contact us with your project scope.
Get a free engineering consultation – we'll evaluate your project within 24 hours. Contact us to discuss details.







