How to Handle File Uploads with Size Limits in Gin

Set Engine.MaxMultipartMemory to define the memory buffer for parsing multipart forms, and optionally check c.Request.ContentLength to reject oversized uploads before processing begins.

Gin processes multipart/form-data requests using the Go standard library’s ParseMultipartForm method, with upload size constraints governed by the Engine.MaxMultipartMemory field. This guide explains how to configure file upload limits in the gin-gonic/gin framework based on the actual source code implementation in gin.go and context.go.

Understanding Gin's Upload Size Mechanism

Gin delegates multipart parsing to the standard library but controls the memory threshold through a configurable engine property. When a handler calls c.FormFile() or c.MultipartForm(), the framework invokes ParseMultipartForm with the limit stored in the engine configuration.

Default Memory Limits and Engine Configuration

The default behavior and configuration points are defined in gin.go:

  • Default limit: Lines 26-27 define defaultMultipartMemory = 32 << 20 (32 MiB)
  • Engine field: Lines 165-167 declare MaxMultipartMemory int64 as a public field on the Engine struct
  • Initialization: Line 221 sets the initial value to defaultMultipartMemory during engine creation

How Parsing Works Under the Hood

When handling uploads, Gin calls c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory) at two key locations in context.go:

  • Lines 997-1002: Inside the FormFile method
  • Lines 1014-1015: Inside the MultipartForm method

The Go standard library stores multipart data up to this size in memory, automatically writing overflow to temporary disk files. If the request exceeds the limit and disk spooling fails (due to permissions or disk space), ParseMultipartForm returns an error that Gin propagates to the caller.

Configuring Global Upload Size Limits

To apply a uniform limit across all routes, modify the MaxMultipartMemory field immediately after creating the engine. This value applies to every multipart request processed by that engine instance.

func main() {
    r := gin.New()
    // Set global limit to 10 MiB
    r.MaxMultipartMemory = 10 << 20

    r.POST("/upload", func(c *gin.Context) {
        file, err := c.FormFile("file")
        if err != nil {
            c.String(http.StatusBadRequest, "Upload failed: %v", err)
            return
        }

        if err := c.SaveUploadedFile(file, "./uploads/"+file.Filename); err != nil {
            c.String(http.StatusInternalServerError, "Save error: %v", err)
            return
        }

        c.String(http.StatusOK, "File %s uploaded successfully", file.Filename)
    })

    r.Run(":8080")
}

Because Engine embeds RouterGroup, all route groups inherit the same engine reference and share this limit. Changing MaxMultipartMemory on the engine affects every route attached to it.

Implementing Per-Route Upload Restrictions

While MaxMultipartMemory is engine-global, you can enforce stricter limits for specific endpoints by pre-validating c.Request.ContentLength before invoking the parser. This prevents unnecessary processing of oversized uploads.

r := gin.New()
r.MaxMultipartMemory = 10 << 20 // Global 10 MiB default

// Strict endpoint with 1 MiB limit
r.POST("/small-upload", func(c *gin.Context) {
    if c.Request.ContentLength > 1<<20 {
        c.String(http.StatusRequestEntityTooLarge, "File exceeds 1 MiB limit")
        return
    }

    file, err := c.FormFile("file")
    if err != nil {
        c.String(http.StatusBadRequest, "Could not process file: %v", err)
        return
    }

    c.SaveUploadedFile(file, "./small/"+file.Filename)
    c.String(http.StatusOK, "Small file uploaded")
})

// Standard endpoint using global 10 MiB limit
r.POST("/regular-upload", func(c *gin.Context) {
    file, _ := c.FormFile("file")
    c.SaveUploadedFile(file, "./regular/"+file.Filename)
    c.String(http.StatusOK, "Regular file uploaded")
})

This approach allows you to define a safe global ceiling while applying stricter business logic to specific routes without modifying the engine configuration.

Handling Upload Errors and Validation

When ParseMultipartForm fails due to memory constraints or disk write errors, Gin returns the error through FormFile or MultipartForm. Implement proper error handling to distinguish between size violations and other parsing failures.

file, err := c.FormFile("upload")
if err != nil {
    // Handle specific error types if needed
    c.String(http.StatusBadRequest, "Upload failed: %v", err)
    return
}

You can also implement pre-emptive rejection by checking the content length against your limit before parsing:

if c.Request.ContentLength > r.MaxMultipartMemory {
    c.String(http.StatusRequestEntityTooLarge,
        "File too large (max %d bytes)", r.MaxMultipartMemory)
    return
}

This prevents the framework from attempting to parse payloads that are guaranteed to exceed your configured limits.

Saving Uploaded Files with Size Constraints

After successful parsing, persist the file using SaveUploadedFile, implemented in context.go at lines 718-724. This helper copies the already-parsed multipart data to your destination path without re-reading the request body.

func handleUpload(c *gin.Context) {
    // Parse with configured memory limit
    file, err := c.FormFile("document")
    if err != nil {
        c.String(http.StatusBadRequest, "Parse error: %v", err)
        return
    }

    // Validate file size if needed
    if file.Size > 5<<20 {
        c.String(http.StatusRequestEntityTooLarge, "File exceeds 5 MiB")
        return
    }

    // Save to disk
    dst := fmt.Sprintf("./uploads/%s", file.Filename)
    if err := c.SaveUploadedFile(file, dst); err != nil {
        c.String(http.StatusInternalServerError, "Save failed: %v", err)
        return
    }

    c.JSON(http.StatusOK, gin.H{"filename": file.Filename, "size": file.Size})
}

Summary

  • Global configuration: Set Engine.MaxMultipartMemory (default 32 MiB) to control the memory buffer for all multipart requests in gin.go
  • Per-route limits: Check c.Request.ContentLength before calling FormFile() to enforce endpoint-specific restrictions
  • Parsing mechanism: Gin calls ParseMultipartForm in context.go (lines 997-1002 and 1014-1015) using the engine's configured limit
  • File persistence: Use SaveUploadedFile (context.go lines 718-724) to write parsed files to disk after validation
  • Error handling: Capture errors from FormFile or MultipartForm to handle cases where uploads exceed available memory and disk space

Frequently Asked Questions

What is the default file upload size limit in Gin?

According to the source code in gin.go lines 26-27, the default limit is 32 MiB (defaultMultipartMemory = 32 << 20). This represents the maximum memory buffer used before spilling to disk, not a hard file size limit.

How do I set different upload limits for specific routes in Gin?

Because MaxMultipartMemory is defined on the Engine struct and shared across all routes, you cannot set different memory limits per route directly. Instead, check c.Request.ContentLength at the beginning of your handler to reject requests that exceed your desired threshold for that specific endpoint before parsing begins.

What happens when an uploaded file exceeds the memory limit?

When a file exceeds MaxMultipartMemory, the Go standard library attempts to write the overflow to temporary disk files. If successful, parsing continues normally. If disk writing fails due to permissions or space constraints, ParseMultipartForm returns an error that Gin propagates through FormFile or MultipartForm, allowing you to handle the failure gracefully.

Can I reject large file uploads before Gin parses the multipart form?

Yes. Check c.Request.ContentLength against your desired limit before calling c.FormFile() or c.MultipartForm(). If the content length exceeds your threshold, return HTTP 413 (Request Entity Too Large) immediately. This prevents the framework from consuming resources to parse payloads you intend to reject anyway.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →