diff --git a/dnsfilter/dnsfilter.go b/dnsfilter/dnsfilter.go
index 82b56187..6a79b79a 100644
--- a/dnsfilter/dnsfilter.go
+++ b/dnsfilter/dnsfilter.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"os"
 	"runtime"
+	"runtime/debug"
 	"strings"
 	"sync"
 
@@ -528,6 +529,9 @@ func (d *Dnsfilter) initFiltering(allowFilters, blockFilters []Filter) error {
 	d.filteringEngine = filteringEngine
 	d.rulesStorageWhite = rulesStorageWhite
 	d.filteringEngineWhite = filteringEngineWhite
+
+	// Make sure that the OS reclaims memory as soon as possible
+	debug.FreeOSMemory()
 	log.Debug("initialized filtering engine")
 
 	return nil
diff --git a/main.go b/main.go
index 36946082..449e3834 100644
--- a/main.go
+++ b/main.go
@@ -1,7 +1,9 @@
 package main
 
 import (
+	"os"
 	"runtime/debug"
+	"time"
 
 	"github.com/AdguardTeam/AdGuardHome/home"
 )
@@ -16,6 +18,32 @@ var channel = "release"
 var goarm = ""
 
 func main() {
-	debug.SetGCPercent(10)
+	memoryUsage()
+
 	home.Main(version, channel, goarm)
 }
+
+// memoryUsage implements a couple of not really beautiful hacks which purpose is to
+// make OS reclaim the memory freed by AdGuard Home as soon as possible.
+func memoryUsage() {
+	debug.SetGCPercent(10)
+
+	// madvdontneed: setting madvdontneed=1 will use MADV_DONTNEED
+	// instead of MADV_FREE on Linux when returning memory to the
+	// kernel. This is less efficient, but causes RSS numbers to drop
+	// more quickly.
+	_ = os.Setenv("GODEBUG", "madvdontneed=1")
+
+	// periodically call "debug.FreeOSMemory" so
+	// that the OS could reclaim the free memory
+	go func() {
+		ticker := time.NewTicker(15 * time.Second)
+		for {
+			select {
+			case t := <-ticker.C:
+				t.Second()
+				debug.FreeOSMemory()
+			}
+		}
+	}()
+}