diff --git a/include/upnp/upnp_context.h b/include/upnp/upnp_context.h
index e29e05bdf24a6bda9c423236d7004b5b7840abcb..da22e25c6c832941a4c82010efc64fdc4b35d1eb 100644
--- a/include/upnp/upnp_context.h
+++ b/include/upnp/upnp_context.h
@@ -165,10 +165,7 @@ private:
     Mapping::sharedPtr_t registerMapping(Mapping& map);
 
     // Remove the given mapping from the local list.
-    //
-    // If the mapping has auto-update enabled, then a new mapping of the same
-    // type will be reserved unless ignoreAutoUpdate is true.
-    void unregisterMapping(const Mapping::sharedPtr_t& map, bool ignoreAutoUpdate = false);
+    void unregisterMapping(const Mapping::sharedPtr_t& map);
 
     // Perform the request on the provided IGD.
     void requestMapping(const Mapping::sharedPtr_t& map);
@@ -231,6 +228,14 @@ private:
     // Process requests with pending status.
     void processPendingRequests();
 
+    // Handle mapping with FAILED state.
+    //
+    // There are two cases to consider: when auto-update is enabled and when it is not.
+    // If auto-update is disabled, the mapping will be unregistered.
+    // If auto-update is enabled, a new mapping of the same type will be requested if there is a valid IGD available.
+    // Otherwise, the mapping request will be marked as pending and will be requested when an IGD becomes available.
+    void handleFailedMapping(const Mapping::sharedPtr_t& map);
+
     // Implementation of UpnpMappingObserver interface.
 
     // Callback used to report changes in IGD status.
diff --git a/src/upnp/upnp_context.cpp b/src/upnp/upnp_context.cpp
index cffab9987df22b665fa806369adcf2f45630a913..b7ec7c99fa633c77f564f3787fed1716b030cc94 100644
--- a/src/upnp/upnp_context.cpp
+++ b/src/upnp/upnp_context.cpp
@@ -209,7 +209,7 @@ UPnPContext::stopUpnp(bool forceRelease)
             // gets recreated if we restart UPnP later.
             map->setState(MappingState::PENDING);
         } else {
-            unregisterMapping(map, true);
+            unregisterMapping(map);
         }
     }
 
@@ -814,7 +814,7 @@ UPnPContext::_syncLocalMappingListWithIgd()
                                                        Mapping::UPNP_MAPPING_DESCRIPTION_PREFIX);
     bool requestsInProgress = false;
     // Use a temporary list to avoid processing mappings while holding the lock.
-    std::list<Mapping::sharedPtr_t> toRemoveFromLocalList;
+    std::list<Mapping::sharedPtr_t> failedMappings;
     for (auto type: {PortType::TCP, PortType::UDP}) {
         std::lock_guard lock(mappingMutex_);
         for (auto& [_, map] : getMappingList(type)) {
@@ -830,10 +830,10 @@ UPnPContext::_syncLocalMappingListWithIgd()
                     auto it = remoteMapList.find(map->getMapKey());
                     if (it == remoteMapList.end()) {
                         if (logger_) logger_->warn("Mapping {} (IGD {}) marked as \"OPEN\" but not found in the "
-                                                   "remote list. Removing from local list.",
+                                                   "remote list. Setting state to \"FAILED\".",
                                                    map->toString(),
                                                    igd->toString());
-                        toRemoveFromLocalList.emplace_back(map);
+                        failedMappings.emplace_back(map);
                     } else {
                         auto oldExpiryTime = map->getExpiryTime();
                         auto newExpiryTime = it->second.getExpiryTime();
@@ -860,11 +860,10 @@ UPnPContext::_syncLocalMappingListWithIgd()
     }
     scheduleMappingsRenewal();
 
-    for (auto const& map : toRemoveFromLocalList) {
+    for (auto const& map : failedMappings) {
         updateMappingState(map, MappingState::FAILED);
-        unregisterMapping(map);
     }
-    if (!toRemoveFromLocalList.empty())
+    if (!failedMappings.empty())
         enforceAvailableMappingsLimits();
 
     if (requestsInProgress) {
@@ -924,7 +923,6 @@ UPnPContext::pruneMappingsWithInvalidIgds(const std::shared_ptr<IGD>& igd)
                  igd->toString(),
                  igd->getProtocolName());
         updateMappingState(map, MappingState::FAILED);
-        unregisterMapping(map);
     }
 }
 
@@ -1130,19 +1128,27 @@ UPnPContext::_endIgdDiscovery()
     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());
+
+    // Use a temporary list to avoid holding the lock while processing the mapping list.
+    // This is necessary because updateMappingState calls a user-defined callback, which
+    // in turn could end up calling a function (such as reserveMapping) that would also
+    // attempt to lock mappingMutex_.
+    std::list<Mapping::sharedPtr_t> toRemoveList;
+    {
+        std::lock_guard lock(mappingMutex_);
+        PortType types[2] {PortType::TCP, PortType::UDP};
+        for (auto type : types) {
+            const auto& mappingList = getMappingList(type);
+            for (const auto& [_, map] : mappingList) {
+                toRemoveList.emplace_back(map);
+            }
         }
     }
+    // We reached the end of the IGD discovery period without finding a valid IGD,
+    // so all mapping requests are considered to have failed.
+    for (auto const& map : toRemoveList) {
+        updateMappingState(map, MappingState::FAILED);
+    }
 }
 
 void
@@ -1204,25 +1210,12 @@ UPnPContext::registerMapping(Mapping& map)
 }
 
 void
-UPnPContext::unregisterMapping(const Mapping::sharedPtr_t& map, bool ignoreAutoUpdate)
+UPnPContext::unregisterMapping(const Mapping::sharedPtr_t& map)
 {
     if (not map) {
         return;
     }
 
-    if (map->getAutoUpdate() && !ignoreAutoUpdate) {
-        if (logger_) logger_->debug("Mapping {} has auto-update enabled, a new mapping will be requested",
-                                    map->toString());
-
-        Mapping newMapping(map->getType());
-        newMapping.enableAutoUpdate(true);
-        newMapping.setNotifyCallback(map->getNotifyCallback());
-        reserveMapping(newMapping);
-
-        // TODO: figure out if this line is actually necessary
-        // (See https://review.jami.net/c/jami-daemon/+/16940)
-        map->setNotifyCallback(nullptr);
-    }
     std::lock_guard lock(mappingMutex_);
     auto& mappingList = getMappingList(map->getType());
 
@@ -1268,7 +1261,6 @@ UPnPContext::onMappingRequestFailed(const Mapping& mapRes)
     }
 
     updateMappingState(map, MappingState::FAILED);
-    unregisterMapping(map);
 
     if (logger_) logger_->warn("Request for mapping {} on IGD {} failed",
               map->toString(),
@@ -1293,6 +1285,39 @@ UPnPContext::updateMappingState(const Mapping::sharedPtr_t& map, MappingState ne
     // Notify the listener if set.
     if (notify and map->getNotifyCallback())
         map->getNotifyCallback()(map);
+
+    if (newState == MappingState::FAILED)
+        handleFailedMapping(map);
+
+}
+
+void
+UPnPContext::handleFailedMapping(const Mapping::sharedPtr_t& map)
+{
+    if (!map->getAutoUpdate()) {
+        // If the mapping is not set to auto-update, it will be unregistered.
+        unregisterMapping(map);
+        return;
+    }
+
+    if (isReady()) {
+        Mapping newMapping(map->getType());
+        newMapping.enableAutoUpdate(true);
+        newMapping.setNotifyCallback(map->getNotifyCallback());
+        reserveMapping(newMapping);
+        if (logger_) logger_->debug("Mapping {} has auto-update enabled, a new mapping will be requested",
+                                    map->toString());
+
+        // TODO: figure out if this line is actually necessary
+        // (See https://review.jami.net/c/jami-daemon/+/16940)
+        map->setNotifyCallback(nullptr);
+    } else {
+        // If there is no valid IGD, the mapping is marked as pending
+        // and will be requested when an IGD becomes available.
+        updateMappingState(map, MappingState::PENDING, false);
+        if (logger_) logger_->debug("Mapping {} will be requested when an IGD becomes available",
+                                    map->toString());
+    }
 }
 
 } // namespace upnp