This episode dissects concurrency in Echo: the request context for values and cancellation, running goroutines inside handlers, context timeouts to bound the duration of work, state synchronization with mutex, and safe patterns for sharing data between requests.

Go is famous for its lightweight goroutines, and Echo uses that power to the fullest. But concurrency is a double-edged sword: without discipline, goroutine leaks, data races, and deadlocks lurk around the corner. This episode teaches how to use concurrency correctly inside Echo.
Episode 12 dissects the request context for values and cancellation, running goroutines inside handlers, context timeouts, state synchronization with mutex, and safe patterns for sharing data between requests.
Every Echo handler has a context accessible through c.Request().Context(). This context is canceled when the request finishes or the client disconnects — an important signal for long-running operations:
func handler(c echo.Context) error {
ctx := c.Request().Context()
user, err := repo.FindByID(ctx, id)
if err != nil {
return err
}
return c.JSON(http.StatusOK, user)
}Always pass ctx to functions that accept a context — the repository from episode 9 was already designed for that. When the client cancels the request, the database operation is canceled along with it.
The context can also carry values that live for a single request. Middleware often uses it to store temporary data:
ctx := context.WithValue(c.Request().Context(), "request_id", reqID)
c.SetRequest(c.Request().WithContext(ctx))Retrieve that value in the handler with the same key. In v5, use c.Set and c.Get for data needed by Echo middleware such as JWT values — covered in episode 13.
Several independent operations can run in parallel. A common pattern: run goroutines, collect results in a channel, combine them once all are done:
type result struct {
user *model.User
posts []*model.Post
err error
}
func handler(c echo.Context) error {
ctx := c.Request().Context()
results := make(chan result, 2)
go func() {
u, err := repo.FindByID(ctx, id)
results <- result{user: u, err: err}
}()
go func() {
p, err := repo.FindPosts(ctx, id)
results <- result{posts: p, err: err}
}()
var user *model.User
var posts []*model.Post
for i := 0; i < 2; i++ {
r := <-results
if r.err != nil {
return r.err
}
if r.user != nil {
user = r.user
}
if r.posts != nil {
posts = r.posts
}
}
return c.JSON(http.StatusOK, map[string]interface{}{
"user": user,
"posts": posts,
})
}A channel with capacity 2 means the goroutines don't wait for the receiver. Don't write the response from a goroutine — only produce values and return them to the main handler.
A hanging request must not run forever. Bound the duration of work with context.WithTimeout:
ctx, cancel := context.WithTimeout(c.Request().Context(), 5*time.Second)
defer cancel()When the timeout is reached, the context is canceled and every operation that respects the context returns an error. This is the second line of defense after WriteTimeout from episode 10.
State accessed by many goroutines must be protected. sync.Mutex is the basic tool:
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}Every access to value goes through mu.Lock. Without it, concurrent increments produce data races that go test -race can detect.
go test -race ./...Run go test -race ./... regularly — Go will report every data race it detects.
High concurrency can overwhelm the database. Combine the rate limiter (episode 6) with caps on parallel work inside the service. For very heavy operations, use a worker pool or a semaphore from golang.org/x/sync:
import "golang.org/x/sync/semaphore"
sem := semaphore.NewWeighted(10)
for _, item := range items {
if err := sem.Acquire(ctx, 1); err != nil {
return err
}
go func(item Item) {
defer sem.Release(1)
process(item)
}(item)
}The semaphore guarantees at most 10 goroutines run at once. This protects the database and other resources from concurrency explosions.
Episode 12 equips you with concurrency discipline: c.Request().Context() carries request cancellation to every operation, goroutines are used with channels to collect results, context.WithTimeout bounds duration, mutex protects shared state, and semaphores limit busyness.
Key takeaways:
c.Request().Context() as the context for every operation.context.WithTimeout bounds the duration of long-running work.go test -race is mandatory to detect races.In episode 13 next, we'll discuss authentication & authorization — JWT with middleware.JWT, Basic Auth, session cookies, refresh tokens, RBAC middleware for access control, and OAuth2 and OpenID Connect integration with Keycloak.