Gin Middleware Not Running in Expected Order
Gin middleware runs in the order it's registered, and confusion about execution order almost always comes down to either registration happening in a different order than assumed, or a misunderstanding of how c.Next() controls when control passes to the next middleware in the chain.
The Problem
You register multiple middleware expecting a specific execution order (for example, authentication before logging, or CORS before everything else), but the actual behavior — visible through logs or side effects — shows them running in a different sequence than expected.
Why It Happens
Gin executes middleware in the exact order they were added via Use(), but several factors commonly cause the actual order to differ from what a developer assumes:
- Global middleware (registered on the main engine with
router.Use()) always runs before route-group-specific middleware, regardless of the order they appear in the source file, if the group-specific ones are registered on a subgroup created afterward - Middleware registered on a route group only applies to routes within that group, so a middleware registered globally is compared against group-level middleware in a way that isn't always visually obvious just from reading the file top to bottom
- A middleware that doesn't call
c.Next()stops the chain there entirely — everything registered after it simply never runs for that request, which can look like an "ordering" problem but is actually the chain being cut short - Code placed after
c.Next()within a middleware function runs on the way back out, after the handler and all subsequent middleware have already executed — this "second half" execution order is often the actual source of confusion, since it's easy to assume a middleware's code all runs upfront
The Fix
First, confirm the actual registration order in your code matches your assumption. Global middleware registered first always executes before global middleware registered after it:
router := gin.New()
router.Use(LoggingMiddleware) // runs first
router.Use(AuthMiddleware) // runs second
For route groups, remember that middleware registered on the parent engine runs before any group-specific middleware, since the group inherits the engine's middleware chain and adds its own on top:
router.Use(GlobalMiddleware) // always runs first for every route
api := router.Group("/api")
api.Use(APISpecificMiddleware) // runs after GlobalMiddleware, only for /api routes
Make sure every middleware that should allow the request to continue actually calls c.Next() — omitting it (unless you're deliberately aborting the request, like for failed authentication) silently prevents everything after it from running:
func LoggingMiddleware(c *gin.Context) {
log.Println("before handler")
c.Next() // without this, nothing after this middleware runs
log.Println("after handler")
}
Understand the two-phase nature of middleware execution: code before c.Next() runs in registration order on the way "in," while code after c.Next() runs in reverse registration order on the way back "out," similar to how a stack unwinds:
// Middleware A registered first, B registered second
// Execution order: A(before) -> B(before) -> handler -> B(after) -> A(after)
This reverse order on the way out is often the actual source of "unexpected order" confusion — code written after c.Next() in the first-registered middleware genuinely runs last, not first, which can be surprising if you're thinking of middleware purely as a simple top-to-bottom list.
If you deliberately want a middleware to stop the chain (like rejecting an unauthenticated request), use c.Abort() explicitly rather than just returning without calling c.Next(), since Abort() makes the intent clear and also prevents any remaining middleware from running even if something later in the chain doesn't check the abort status itself:
func AuthMiddleware(c *gin.Context) {
if !isAuthenticated(c) {
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
return
}
c.Next()
}
Still Not Working?
If the order still doesn't match expectations after reviewing registration and c.Next() usage, add explicit logging at the very start and end of each middleware function temporarily, printing the middleware's name — this makes the actual execution order completely unambiguous in your logs, rather than relying on inference from application behavior that might be affected by multiple factors at once.