
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