diff --git a/include/upnp/upnp_context.h b/include/upnp/upnp_context.h
index fca38a530b7df0fc1795aade85db5cb6469186be..d16105372438f9873846dcbdb830b154548f7f98 100644
--- a/include/upnp/upnp_context.h
+++ b/include/upnp/upnp_context.h
@@ -75,6 +75,7 @@ public:
     virtual void onMappingRequestFailed(const Mapping& map) = 0;
     virtual void onMappingRenewed(const std::shared_ptr<IGD>& igd, const Mapping& map) = 0;
     virtual void onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& map) = 0;
+    virtual void onIgdDiscoveryStarted() = 0;
 };
 
 class UPnPContext : public UpnpMappingObserver
@@ -133,6 +134,11 @@ public:
         });
     }
 
+    // Set the timeout for the IGD discovery process.
+    // If the timeout expires and no valid IGD has been discovered,
+    // then the state of all pending mappings is set to FAILED.
+    void setIgdDiscoveryTimeout(std::chrono::milliseconds timeout);
+
 private:
     // Initialization
     void init();
@@ -242,6 +248,9 @@ private:
     // Callback used to report remove request status.
     void onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& map) override;
 
+    // Callback used to report the start of the discovery process: search for IGDs.
+    void onIgdDiscoveryStarted() override;
+
 private:
     UPnPContext(const UPnPContext&) = delete;
     UPnPContext(UPnPContext&&) = delete;
@@ -304,6 +313,16 @@ private:
     // Shutdown synchronization
     bool shutdownComplete_ {false};
     bool shutdownTimedOut_ {false};
+
+    // IGD Discovery synchronization. This boolean indicates if the IGD discovery is in progress.
+    bool igdDiscoveryInProgress_ {true};
+    std::mutex igdDiscoveryMutex_;
+    std::chrono::milliseconds igdDiscoveryTimeout_ {std::chrono::milliseconds(500)};
+
+    // End of the discovery process.
+    void _endIgdDiscovery();
+
+    asio::steady_timer igdDiscoveryTimer_;
 };
 
 } // namespace upnp
diff --git a/src/upnp/protocol/natpmp/nat_pmp.cpp b/src/upnp/protocol/natpmp/nat_pmp.cpp
index 3c17e09e07a22f0c45cd8b536a88c13a9fa92de0..9005ee4a4e8801ff0335040ff0c19537b2f33355 100644
--- a/src/upnp/protocol/natpmp/nat_pmp.cpp
+++ b/src/upnp/protocol/natpmp/nat_pmp.cpp
@@ -189,6 +189,7 @@ void
 NatPmp::searchForIgd()
 {
     if (not initialized_) {
+        observer_->onIgdDiscoveryStarted();
         initNatPmp();
     }
 
@@ -196,7 +197,6 @@ NatPmp::searchForIgd()
     if (not initialized_) {
         if (igdSearchCounter_++ < MAX_RESTART_SEARCH_RETRIES) {
             if (logger_) logger_->debug("NAT-PMP: Start search for IGDs. Attempt {}", igdSearchCounter_);
-
             // Cancel the current timer (if any) and re-schedule.
             searchForIgdTimer_.expires_after(NATPMP_SEARCH_RETRY_UNIT * igdSearchCounter_);
             searchForIgdTimer_.async_wait([w=weak()](const asio::error_code& ec) {
diff --git a/src/upnp/protocol/pupnp/pupnp.cpp b/src/upnp/protocol/pupnp/pupnp.cpp
index c5a54e6990fd8b3cf0c31a7da3f28d169bf8ff98..afb110a8778731d9bea7803cb76ed16d485143e1 100644
--- a/src/upnp/protocol/pupnp/pupnp.cpp
+++ b/src/upnp/protocol/pupnp/pupnp.cpp
@@ -240,6 +240,35 @@ PUPnP::terminate()
     if (logger_) logger_->debug("PUPnP: Instance {} terminated", fmt::ptr(this));
 }
 
+void
+PUPnP::searchForDeviceAsync(const std::string& deviceType)
+{
+    // Despite its name and the claim in the libupnp documentation that it "returns immediately",
+    // the UpnpSearchAsync function isn't really async. This is because it tries to send multiple
+    // copies of each search message and waits for a certain amount of time after sending each
+    // copy. The number of copies is given by the NUM_SSDP_COPY macro, whose default value is 2,
+    // and the waiting time is determined by the SSDP_PAUSE macro, whose default value is 100 (ms).
+    // If both IPv4 and IPv6 are enabled, then UpnpSearchAsync sends 3 distinct messages (2 for IPv6
+    // and 1 for IPv4), resulting in a total of 3 * 2 * 100 = 600 ms spent waiting by default.
+    // This is why we put the call to UpnpSearchAsync on its own thread.
+    dht::ThreadPool::io().run([w = weak_from_this(), deviceType] {
+        auto sthis = std::static_pointer_cast<PUPnP>(w.lock());
+        if (!sthis)
+            return;
+
+        auto err = UpnpSearchAsync(sthis->ctrlptHandle_,
+                                   SEARCH_TIMEOUT,
+                                   deviceType.c_str(),
+                                   sthis.get());
+        if (err != UPNP_E_SUCCESS) {
+            if (sthis->logger_)
+                sthis->logger_->warn("PUPnP: Send search for {} failed. Error {:d}: {}",
+                                     deviceType,
+                                     err,
+                                     UpnpGetErrorMessage(err));
+        }
+    });
+}
 void
 PUPnP::searchForDevices()
 {
@@ -247,34 +276,10 @@ PUPnP::searchForDevices()
 
     // Send out search for multiple types of devices, as some routers may possibly
     // only reply to one.
-
-    auto err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_ROOT_DEVICE, this);
-    if (err != UPNP_E_SUCCESS) {
-        if (logger_) logger_->warn("PUPnP: Send search for UPNP_ROOT_DEVICE failed. Error {:d}: {}",
-                  err,
-                  UpnpGetErrorMessage(err));
-    }
-
-    err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_IGD_DEVICE, this);
-    if (err != UPNP_E_SUCCESS) {
-        if (logger_) logger_->warn("PUPnP: Send search for UPNP_IGD_DEVICE failed. Error {:d}: {}",
-                  err,
-                  UpnpGetErrorMessage(err));
-    }
-
-    err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANIP_SERVICE, this);
-    if (err != UPNP_E_SUCCESS) {
-        if (logger_) logger_->warn("PUPnP: Send search for UPNP_WANIP_SERVICE failed. Error {:d}: {}",
-                  err,
-                  UpnpGetErrorMessage(err));
-    }
-
-    err = UpnpSearchAsync(ctrlptHandle_, SEARCH_TIMEOUT, UPNP_WANPPP_SERVICE, this);
-    if (err != UPNP_E_SUCCESS) {
-        if (logger_) logger_->warn("PUPnP: Send search for UPNP_WANPPP_SERVICE failed. Error {:d}: {}",
-                  err,
-                  UpnpGetErrorMessage(err));
-    }
+    searchForDeviceAsync(UPNP_ROOT_DEVICE);
+    searchForDeviceAsync(UPNP_IGD_DEVICE);
+    searchForDeviceAsync(UPNP_WANIP_SERVICE);
+    searchForDeviceAsync(UPNP_WANPPP_SERVICE);
 }
 
 void
@@ -340,6 +345,7 @@ PUPnP::searchForIgd()
         if (clientRegistered_) {
             assert(initialized_);
             searchForDevices();
+            observer_->onIgdDiscoveryStarted();
         } else {
             if (logger_) logger_->warn("PUPnP: PUPNP not fully setup. Skipping the IGD search");
         }
diff --git a/src/upnp/protocol/pupnp/pupnp.h b/src/upnp/protocol/pupnp/pupnp.h
index 32fe19a1ed45c9898f8d8c3d38c3164fd4585e04..ae02d45ac4e07c1ac360c8233ea3ac8e996b17ae 100644
--- a/src/upnp/protocol/pupnp/pupnp.h
+++ b/src/upnp/protocol/pupnp/pupnp.h
@@ -134,6 +134,9 @@ private:
     // Start search for UPNP devices
     void searchForDevices();
 
+    // Start search for UPNP device in a different thread
+    void searchForDeviceAsync(const std::string& deviceType);
+
     // Return true if it has at least one valid IGD.
     bool hasValidIgd() const;
 
diff --git a/src/upnp/upnp_context.cpp b/src/upnp/upnp_context.cpp
index e832c65e2ffe7bf3930f016d0407373fe922cd9c..47ff9d9c3962dc26101df014b143336c26d5a08a 100644
--- a/src/upnp/upnp_context.cpp
+++ b/src/upnp/upnp_context.cpp
@@ -51,6 +51,8 @@ UPnPContext::UPnPContext(const std::shared_ptr<asio::io_context>& ioContext, con
  , renewalSchedulingTimer_(*ctx)
  , syncTimer_(*ctx)
  , connectivityChangedTimer_(*ctx)
+ , igdDiscoveryTimer_(*ctx)
+
 {
     if (logger_) logger_->debug("Creating UPnPContext instance [{}]", fmt::ptr(this));
 
@@ -1095,6 +1097,51 @@ UPnPContext::onMappingRemoved(const std::shared_ptr<IGD>& igd, const Mapping& ma
         map->getNotifyCallback()(map);
 }
 
+void
+UPnPContext::onIgdDiscoveryStarted()
+{
+    std::lock_guard lock(igdDiscoveryMutex_);
+    igdDiscoveryInProgress_ = true;
+    if (logger_) logger_->debug("IGD Discovery started");
+    igdDiscoveryTimer_.expires_after(igdDiscoveryTimeout_);
+    igdDiscoveryTimer_.async_wait([this] (const asio::error_code& ec) {
+        if (ec != asio::error::operation_aborted && igdDiscoveryInProgress_) {
+            _endIgdDiscovery();
+        }
+    });
+}
+
+void
+UPnPContext::_endIgdDiscovery()
+{
+    std::lock_guard lockDiscovery_(igdDiscoveryMutex_);
+    igdDiscoveryInProgress_ = false;
+    if (logger_) logger_->debug("IGD Discovery ended");
+    if (isReady()) {
+       return;
+    }
+    // if there is no valid IGD, the pending mapping requests will be changed to failed
+    std::lock_guard lockMappings_(mappingMutex_);
+    PortType types[2] {PortType::TCP, PortType::UDP};
+    for (auto& type : types) {
+        const auto& mappingList = getMappingList(type);
+        for (auto const& [_, map] : mappingList) {
+            updateMappingState(map, MappingState::FAILED);
+            // Do not unregister the mapping, it's up to the controller to decide. It will be unregistered when the controller releases it.
+            // unregisterMapping(map) here will cause a deadlock because of the lock on mappingMutex_.
+            if (logger_) logger_->warn("Request for mapping {} failed, no IGD available",
+                                       map->toString());
+        }
+    }
+}
+
+void
+UPnPContext::setIgdDiscoveryTimeout(std::chrono::milliseconds timeout)
+{
+    std::lock_guard lock(igdDiscoveryMutex_);
+    igdDiscoveryTimeout_ = timeout;
+}
+
 Mapping::sharedPtr_t
 UPnPContext::registerMapping(Mapping& map)
 {
@@ -1123,14 +1170,23 @@ UPnPContext::registerMapping(Mapping& map)
         assert(mapPtr);
     }
 
-    // No available IGD. The pending mapping requests will be processed
-    // when an IGD becomes available
     if (not isReady()) {
-        if (logger_) logger_->warn("No IGD available. Mapping will be requested when an IGD becomes available");
+        // There is no valid IGD available
+        std::lock_guard lock(igdDiscoveryMutex_);
+        // IGD discovery is in progress, the mapping request will be made once an IGD becomes available
+        if (igdDiscoveryInProgress_) {
+            if (logger_) logger_->debug("Request for mapping {} will be requested when an IGD becomes available",
+                                        map.toString());
+        } else {
+            // it's not in the IGD discovery phase, the mapping request will fail
+            if (logger_) logger_->warn("Request for mapping {} failed, no IGD available",
+                                       map.toString());
+            updateMappingState(mapPtr, MappingState::FAILED);
+        }
     } else {
+        // There is a valid IGD available, request the mapping.
         requestMapping(mapPtr);
     }
-
     return mapPtr;
 }