Skip to content
Go back

Why Multipart File Uploads Break in Background Goroutines (Gin + Go)

Edit page

Multipart upload race condition

When building APIs in Go, it’s common to return a response quickly and offload heavy uploading a large file to a background goroutine.

Sometimes the file upload succeeded, sometimes it failed because the file was “missing”. This post explains why this happens, what’s really going on, and how to fix it.

The Problem

The handler below processes user data immediately and uploads an optional file in a background goroutine.

The request should return instantly—the user doesn’t need to wait for the file upload to finish.

type UploadRequest struct {
    File *multipart.FileHeader `form:"file"`
    UserName String  `form:"user_name"`
}

func handler(c *gin.Context) {
    var req UploadRequest
    if err := c.ShouldBind(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }

    var response = processUserData(req)
    
    //After process data we wont need to wait for file upload.
    go uploadToBlob(req.File)

    c.JSON(http.StatusOK, response)
}

What’s Actually Happening

multipart.FileHeader is not the actual file. It only contains metadata and a pointer to a temporary file stored on disk. When the handler function returns, this temporary file is cleaned up. This cleanup occurs even if the file reference is stored inside a struct.

	go uploadToBlob(req.File)

This line of code runs after the file has already been cleaned up. As a result, sometimes the file still exists, and sometimes it is already gone. This behavior depends on file size: large files take longer to process, which makes the race condition visible, while small files finish quickly, so the code appears to work intermittently.

To fix this issue, I store the uploaded file in a new temporary file that I control:

    tempFile, err := os.CreateTemp(
	    tempDir,
	    fmt.Sprintf("temp-[%s]-*%s", file.Filename, fileExtension),
	)

By creating my own temporary file, I take ownership of the file lifecycle, ensuring it is not removed when the HTTP request finishes.

After the background job finishes, I explicitly remove the temporary files I created. Since I own the file lifecycle, cleanup is now deterministic and no longer tied to the HTTP request.


	 err := os.Remove(f.Path);

Multipart Automatically Cleans Up Temporary Files

According to net/http, once the request finishes, Go automatically removes these temporary files by calling:

func (w *response) finishRequest() {
    ...
	w.req.MultipartForm.RemoveAll()
    

This function deletes all temporary files associated with the multipart form.

Conclusion

Multipart upload files in Gin are temporary and automatically deleted when the request ends, so background goroutines must never rely on them without first persisting or copying the file.

Reference:

https://pkg.go.dev/mime/multipart#Form.RemoveAll https://github.com/golang/go/blob/release-branch.go1.10/src/net/http/server.go#L1553-L1555


Edit page
Share this post on:

Next Post
How to exposing TCP services on kubernetes ingress-nginx