diff --git a/CMakeLists.txt b/CMakeLists.txt
index e4386cedf..00642c13d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -371,6 +371,7 @@ check_function_exists(backtrace_symbols HAVE_BACKTRACE_SYMBOLS)
check_function_exists(pipe2 HAVE_PIPE2)
check_function_exists(nice HAVE_NICE)
check_function_exists(malloc_info HAVE_MALLOC_INFO)
+check_function_exists(malloc_trim HAVE_MALLOC_TRIM)
check_function_exists(pthread_set_name_np HAVE_PTHREAD_SET_NAME_NP)
check_function_exists(pthread_setname_np HAVE_PTHREAD_SETNAME_NP)
check_library_exists(dl dladdr "dlfcn.h" HAVE_DLADDR)
diff --git a/config.h.cmake b/config.h.cmake
index 2bfadb4a0..6171df0d7 100644
--- a/config.h.cmake
+++ b/config.h.cmake
@@ -9,6 +9,7 @@
#cmakedefine HAVE_CXXABI_H
#cmakedefine HAVE_NICE
#cmakedefine HAVE_MALLOC_INFO
+#cmakedefine HAVE_MALLOC_TRIM
#cmakedefine HAVE_PTHREAD_SET_NAME_NP
#cmakedefine HAVE_PTHREAD_SETNAME_NP
#cmakedefine HAVE_EDITLINE
diff --git a/doc/12-icinga2-api.md b/doc/12-icinga2-api.md
index fad9f0700..b56beb515 100644
--- a/doc/12-icinga2-api.md
+++ b/doc/12-icinga2-api.md
@@ -2594,6 +2594,40 @@ but the raw XML output from `malloc_info(3)`. See also the
```
+### Memory Usage Reduction
+
+The GNU libc function `malloc_trim(3)` attempts to release free memory
+from the main heap arena of Icinga 2 itself. You can call it directly
+by sending a `POST` request to the URL endpoint `/v1/debug/malloc_trim`.
+
+The following parameters may be specified
+(either as URL parameters or in a JSON-encoded message body):
+
+ Parameter | Type | Description
+ ----------|--------|-------------
+ pad | Number | **Optional.** How many heap bytes to preserve, so the next `malloc(3)` call doesn't need to re-allocate memory again via `sbrk(2)`. Defaults to 0.
+
+The [API permission](12-icinga2-api.md#icinga2-api-permissions) `debug` is required.
+
+Example:
+
+```bash
+curl -k -s -S -i -u root:icinga -H 'Accept: application/json' \
+ -X POST 'https://localhost:5665/v1/debug/malloc_trim?pad=1'
+```
+
+```json
+{
+ "results": [
+ {
+ "code": 200.0,
+ "malloc_trim": 1.0,
+ "status": "Some memory was released back to the system."
+ }
+ ]
+}
+```
+
## API Clients
After its initial release in 2015, community members
diff --git a/lib/remote/CMakeLists.txt b/lib/remote/CMakeLists.txt
index d8d3298c5..f39a2f56b 100644
--- a/lib/remote/CMakeLists.txt
+++ b/lib/remote/CMakeLists.txt
@@ -34,6 +34,7 @@ set(remote_SOURCES
jsonrpc.cpp jsonrpc.hpp
jsonrpcconnection.cpp jsonrpcconnection.hpp jsonrpcconnection-heartbeat.cpp jsonrpcconnection-pki.cpp
mallocinfohandler.cpp mallocinfohandler.hpp
+ malloctrimhandler.cpp malloctrimhandler.hpp
messageorigin.cpp messageorigin.hpp
modifyobjecthandler.cpp modifyobjecthandler.hpp
objectqueryhandler.cpp objectqueryhandler.hpp
diff --git a/lib/remote/malloctrimhandler.cpp b/lib/remote/malloctrimhandler.cpp
new file mode 100644
index 000000000..bf450ca5c
--- /dev/null
+++ b/lib/remote/malloctrimhandler.cpp
@@ -0,0 +1,84 @@
+/* Icinga 2 | (c) 2024 Icinga GmbH | GPLv2+ */
+
+#include "remote/filterutility.hpp"
+#include "remote/httputility.hpp"
+#include "remote/malloctrimhandler.hpp"
+#include
+#include
+
+#ifdef HAVE_MALLOC_TRIM
+# include
+#endif /* HAVE_MALLOC_TRIM */
+
+using namespace icinga;
+
+REGISTER_URLHANDLER("/v1/debug/malloc_trim", MallocTrimHandler);
+
+bool MallocTrimHandler::HandleRequest(
+ AsioTlsStream&,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request& request,
+ const Url::Ptr& url,
+ boost::beast::http::response& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context&,
+ HttpServerConnection&
+)
+{
+ namespace http = boost::beast::http;
+
+ if (url->GetPath().size() != 3) {
+ return false;
+ }
+
+ if (request.method() != http::verb::post) {
+ return false;
+ }
+
+ auto rawPad (HttpUtility::GetLastParameter(params, "pad"));
+ size_t pad = 0;
+
+ if (rawPad.GetType() != ValueEmpty) {
+ try {
+ pad = boost::lexical_cast(rawPad);
+ } catch (const std::exception&) {
+ HttpUtility::SendJsonError(response, params, 400,
+ "Invalid 'pad' specified. An integer [0," BOOST_PP_STRINGIZE(SIZE_MAX) "] is required.");
+
+ return true;
+ }
+ }
+
+ FilterUtility::CheckPermission(user, "debug");
+
+#ifndef HAVE_MALLOC_TRIM
+ HttpUtility::SendJsonError(response, params, 501, "malloc_trim(3) not available.");
+#else /* HAVE_MALLOC_TRIM */
+ Dictionary::Ptr result1;
+ auto ret (malloc_trim(pad));
+
+ if (ret) {
+ result1 = new Dictionary({
+ { "code", 200 },
+ { "malloc_trim", ret },
+ { "status", "Some memory was released back to the system." }
+ });
+ } else {
+ result1 = new Dictionary({
+ { "code", 503 },
+ { "malloc_trim", ret },
+ { "status", "It was not possible to release any memory." }
+ });
+
+ response.result(http::status::service_unavailable);
+ }
+
+ Dictionary::Ptr result = new Dictionary({
+ { "results", new Array({ result1 }) }
+ });
+
+ HttpUtility::SendJsonBody(response, params, result);
+#endif /* HAVE_MALLOC_TRIM */
+
+ return true;
+}
diff --git a/lib/remote/malloctrimhandler.hpp b/lib/remote/malloctrimhandler.hpp
new file mode 100644
index 000000000..9622d4684
--- /dev/null
+++ b/lib/remote/malloctrimhandler.hpp
@@ -0,0 +1,27 @@
+/* Icinga 2 | (c) 2024 Icinga GmbH | GPLv2+ */
+
+#pragma once
+
+#include "remote/httphandler.hpp"
+
+namespace icinga
+{
+
+class MallocTrimHandler final : public HttpHandler
+{
+public:
+ DECLARE_PTR_TYPEDEFS(MallocTrimHandler);
+
+ bool HandleRequest(
+ AsioTlsStream& stream,
+ const ApiUser::Ptr& user,
+ boost::beast::http::request& request,
+ const Url::Ptr& url,
+ boost::beast::http::response& response,
+ const Dictionary::Ptr& params,
+ boost::asio::yield_context& yc,
+ HttpServerConnection& server
+ ) override;
+};
+
+}