Connection leak issue with Mongo-Go Driver

I’m developing a Go application that integrates with MongoDB to create, read, and delete images. I’ve set the connection pool to 4 for testing purposes, but I’ve noticed that the number of connections can go up to 10 even if I haven’t hit any endpoint related to MongoDB. If I don’t hit any endpoint, the number of connections stays between 2-3. There’s a connection leak in my code, and I’m having trouble figuring out how to release resources correctly to ensure that they are returned to the pool.

As the documentation says mongo-go driver is goroutine safe so I have a function that initialize mongo and set a global variable with the initialized mongo client

package persistence

import (
	"context"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"time"
)

var MongoClient *mongo.Client

func InitMongo(ctx context.Context, URI string) error {
	if MongoClient != nil {
		return nil
	}
	serverAPI := options.ServerAPI(options.ServerAPIVersion1)
	opts := options.Client().ApplyURI(URI).SetServerAPIOptions(serverAPI)

	opts.SetMinPoolSize(10)
	opts.SetMaxPoolSize(100)
	opts.SetMaxConnIdleTime(2 * time.Second)

	// Create a new client and connect to the server
	client, err := mongo.Connect(ctx, opts)
	if err != nil {
		return err
	}
	MongoClient = client

	// Send a ping to confirm a successful connection
	var result bson.M
	if err = client.Database("admin").RunCommand(context.TODO(), bson.D{{"ping", 1}}).Decode(&result); err != nil {
		return err
	}

	return nil

}

and I use this function in the main to initialize it

func main() {
	configuration := config.NewEnvConfigs()

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	err = persistence.InitMongo(ctx, configuration.MongoDBURI)
	if err != nil {
		fmt.Println("Mongo not ready")
		panic(err)
	}
	fmt.Println("mongo connected")
	defer func() {
		if err = persistence.MongoClient.Disconnect(context.TODO()); err != nil {
			panic(err)
		}
	}()
	r := server.Routes()
	http.ListenAndServe("0.0.0.0:3333", r)
}

also the last piece of code that uses the mongo client is the handlers for images endpoints

package handlers

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"github.com/bycultivaet/backend/internal/infrastructure/persistence"
	"github.com/go-chi/chi"
	"github.com/go-chi/render"
	"github.com/google/uuid"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"io/ioutil"
	"mime/multipart"
	"net/http"
)

func GetImageByID() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx, cancel := context.WithCancel(r.Context())
		defer cancel()
		Uuid := chi.URLParam(r, "uuid")
		_, err := uuid.Parse(Uuid)
		if err != nil {
			render.Status(r, 400)
			render.Respond(w, r, errors.New("invalid uuid").Error())
			return
		}
		filter := bson.M{"uuid": Uuid}
		var result bson.M
		collection := persistence.MongoClient.Database("hassad-media").Collection("images")
		err = collection.FindOne(ctx, filter).Decode(&result)
		if err != nil {
			render.Status(r, 404)
			render.Respond(w, r, errors.New("image not found").Error())
			return
		}
		imageData, ok := result["image"].(primitive.Binary)
		if !ok {
			render.Status(r, 500)
			render.Respond(w, r, errors.New("error parsing image").Error())
			return
		}
		imageBase64 := base64.StdEncoding.EncodeToString(imageData.Data)
		render.Status(r, 200)
		render.JSON(w, r, map[string]string{"image": imageBase64})
	}
}

func UploadImage() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx, cancel := context.WithCancel(r.Context())
		defer cancel()
		cancelled := r.Context().Done()
		select {
		case <-cancelled:
			render.Status(r, 499)
			render.Respond(w, r, errors.New("request cancelled").Error())
			return
		default:
			err := r.ParseMultipartForm(10 << 20)
			if err != nil {
				render.Status(r, http.StatusBadRequest)
				render.Respond(w, r, err.Error())
				return
			}
			file, _, err := r.FormFile("image")
			if err != nil {
				render.Status(r, http.StatusBadRequest)
				render.Respond(w, r, err.Error())
				return
			}
			defer file.Close()
			Uuid, err := uuid.NewRandom()
			if err != nil {
				render.Status(r, http.StatusInternalServerError)
				render.Respond(w, r, errors.New("error generating uuid").Error())
				return
			}
			// Pass the contents of the file to GetMongoDB
			_, err = UploadPhoto(file, Uuid.String(), ctx)
			if err != nil {
				render.Status(r, http.StatusInternalServerError)
				render.Respond(w, r, err.Error())
				return
			}

			render.Status(r, http.StatusOK)
			render.JSON(w, r, map[string]string{"uuid": Uuid.String()})

		}
	}
}

func UploadPhoto(file multipart.File, uuid string, ctx context.Context) (interface{}, error) {
	imageBytes, err := ioutil.ReadAll(file)
	if err != nil {
		return nil, err
	}
	imageDoc := bson.M{"image": imageBytes, "uuid": uuid}
	defer file.Close()
	collection := persistence.MongoClient.Database("hassad-media").Collection("images")
	data, err := collection.InsertOne(ctx, imageDoc)
	if err != nil {
		return 0, err
	}

	fmt.Println("Inserted image into MongoDB!")
	fmt.Println(data.InsertedID)
	return data.InsertedID, nil
}

func DeleteImageByID() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx, cancel := context.WithCancel(r.Context())
		defer cancel()

		uuid := chi.URLParam(r, "uuid")
		filter := bson.M{"uuid": uuid}
		collection := persistence.MongoClient.Database("hassad-media").Collection("images")
		res, err := collection.DeleteOne(ctx, filter)
		if err != nil {
			if res.DeletedCount == 0 {
				render.Status(r, 404)
				render.Respond(w, r, errors.New("image not found").Error())
				return
			}
			render.Status(r, 500)
			render.Respond(w, r, err.Error())
			return
		}
		render.JSON(w, r, map[string]string{"answer": "deleted"})
	}
}

what I’m doing wrong closing the connections to be returned to the pool or any other configuration

I test my code by with python script that hits the endpoints excessively

Hey @Omar_Dawah, welcome and thanks for the question!

What you’re describing actually sounds like normal behavior. Additionally, the code you provided seems correct and shouldn’t cause a connection leak.

MongoDB drivers open a minimum of 2 monitoring connections to each node in a MongoDB database, plus at least 1 connection to each node for user operations. For example, if you’re connecting to a 3-node replica set, MongoDB drivers will open 6 total monitoring connections before you run any operations. Monitoring connections are used to track the state of the database to maximize availability during topology changes or unexpected disconnects.

Even though your description doesn’t sound like a connection leak, I have a few questions to help me understand your situation:

  1. What version of the Go driver are you using?
  2. What MongoDB topology are you connecting to? For example, is it a standalone node, a replica set, a sharded cluster, Atlas Serverless, or something else?
  3. Are you connecting to a self-hosted or Atlas-hosted database?

Thanks!

2 Likes

Thanks For Answering @Matt_Dale,
1- go.mongodb.org/mongo-driver v1.12.0
2- Mongo Cluster (The Free plan)
3- Atlas-hosted database

Another piece of information that may help you, is that on production I used to get connections up to 500 even if the max pool is 100 but after I have set opts.SetMaxConnIdleTime(2 * time.Second) it’s dropped to about 240-250 at the peak of using.
also, I’m sure that there are no 240 hits to the Mongo endpoints