diff --git a/src/connectivity/Makefile.am b/src/connectivity/Makefile.am
index d16c5cc4dc359167b0b98e3b85f55309842796c5..3c8f482f2f6191e54e4f8085caef526679b7bcec 100644
--- a/src/connectivity/Makefile.am
+++ b/src/connectivity/Makefile.am
@@ -18,6 +18,10 @@ libconnectivity_la_SOURCES = \
 		./connectivity/peer_connection.h \
 		./connectivity/sip_utils.cpp \
 		./connectivity/sip_utils.h \
+		./connectivity/turn_transport.cpp \
+		./connectivity/turn_transport.h \
+		./connectivity/turn_cache.cpp \
+		./connectivity/turn_cache.h \
 		./connectivity/utf8_utils.cpp \
 		./connectivity/utf8_utils.h \
 		./connectivity/transport/peer_channel.h
diff --git a/src/connectivity/peer_connection.h b/src/connectivity/peer_connection.h
index be7f102961c3e39ec6dbf51ea1b56aa54d6d4e23..b6d010b03658f7074f4fd9169785500f0a55f05e 100644
--- a/src/connectivity/peer_connection.h
+++ b/src/connectivity/peer_connection.h
@@ -52,9 +52,6 @@ using OnStateChangeCb = std::function<bool(tls::TlsSessionState state)>;
 using OnReadyCb = std::function<void(bool ok)>;
 using onShutdownCb = std::function<void(void)>;
 
-class TurnTransport;
-class ConnectedTurnTransport;
-
 //==============================================================================
 
 class Stream
diff --git a/src/connectivity/turn_cache.cpp b/src/connectivity/turn_cache.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..2a6d856f92c6421fecf115af063f0d5312190d0d
--- /dev/null
+++ b/src/connectivity/turn_cache.cpp
@@ -0,0 +1,196 @@
+/*
+ *  Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#include "connectivity/turn_cache.h"
+
+#include "logger.h"
+#include "fileutils.h"
+#include "manager.h"
+#include "opendht/thread_pool.h" // TODO remove asio
+
+namespace jami {
+
+TurnCache::TurnCache(const std::string& accountId,
+                     const std::string& cachePath,
+                     const TurnTransportParams& params,
+                     bool enabled)
+    : accountId_(accountId)
+    , cachePath_(cachePath)
+    , io_context(Manager::instance().ioContext())
+{
+    refreshTimer_ = std::make_unique<asio::steady_timer>(*io_context,
+                                                         std::chrono::steady_clock::now());
+    reconfigure(params, enabled);
+}
+
+TurnCache::~TurnCache() {}
+
+std::optional<IpAddr>
+TurnCache::getResolvedTurn(uint16_t family) const
+{
+    if (family == AF_INET && cacheTurnV4_) {
+        return *cacheTurnV4_;
+    } else if (family == AF_INET6 && cacheTurnV6_) {
+        return *cacheTurnV6_;
+    }
+    return std::nullopt;
+}
+
+void
+TurnCache::reconfigure(const TurnTransportParams& params, bool enabled)
+{
+    params_ = params;
+    enabled_ = enabled;
+    {
+        std::lock_guard<std::mutex> lk(cachedTurnMutex_);
+        // Force re-resolution
+        isRefreshing_ = false;
+        cacheTurnV4_.reset();
+        cacheTurnV6_.reset();
+        testTurnV4_.reset();
+        testTurnV6_.reset();
+    }
+    refreshTimer_->expires_at(std::chrono::steady_clock::now());
+    refreshTimer_->async_wait(std::bind(&TurnCache::refresh, this, std::placeholders::_1));
+}
+
+void
+TurnCache::refresh(const asio::error_code& ec)
+{
+    if (ec == asio::error::operation_aborted)
+        return;
+    // The resolution of the TURN server can take quite some time (if timeout).
+    // So, run this in its own io thread to avoid to block the main thread.
+    // Avoid multiple refresh
+    if (isRefreshing_.exchange(true))
+        return;
+    if (!enabled_) {
+        // In this case, we do not use any TURN server
+        std::lock_guard<std::mutex> lk(cachedTurnMutex_);
+        cacheTurnV4_.reset();
+        cacheTurnV6_.reset();
+        isRefreshing_ = false;
+        return;
+    }
+    JAMI_INFO("[Account %s] Refresh cache for TURN server resolution", accountId_.c_str());
+    // Retrieve old cached value if available.
+    // This means that we directly get the correct value when launching the application on the
+    // same network
+    // No need to resolve, it's already a valid address
+    auto server = params_.domain;
+    if (IpAddr::isValid(server, AF_INET)) {
+        testTurn(IpAddr(server, AF_INET));
+        return;
+    } else if (IpAddr::isValid(server, AF_INET6)) {
+        testTurn(IpAddr(server, AF_INET6));
+        return;
+    }
+    // Else cache resolution result
+    fileutils::recursive_mkdir(cachePath_ + DIR_SEPARATOR_STR + "domains", 0700);
+    auto pathV4 = cachePath_ + DIR_SEPARATOR_STR + "domains" + DIR_SEPARATOR_STR + "v4." + server;
+    IpAddr testV4, testV6;
+    if (auto turnV4File = std::ifstream(pathV4)) {
+        std::string content((std::istreambuf_iterator<char>(turnV4File)),
+                            std::istreambuf_iterator<char>());
+        testV4 = IpAddr(content, AF_INET);
+    }
+    auto pathV6 = cachePath_ + DIR_SEPARATOR_STR + "domains" + DIR_SEPARATOR_STR + "v6." + server;
+    if (auto turnV6File = std::ifstream(pathV6)) {
+        std::string content((std::istreambuf_iterator<char>(turnV6File)),
+                            std::istreambuf_iterator<char>());
+        testV6 = IpAddr(content, AF_INET6);
+    }
+    // Resolve just in case. The user can have a different connectivity
+    auto turnV4 = IpAddr {server, AF_INET};
+    {
+        if (turnV4) {
+            // Cache value to avoid a delay when starting up Jami
+            std::ofstream turnV4File(pathV4);
+            turnV4File << turnV4.toString();
+        } else
+            fileutils::remove(pathV4, true);
+        // Update TURN
+        testV4 = IpAddr(std::move(turnV4));
+    }
+    auto turnV6 = IpAddr {server, AF_INET6};
+    {
+        if (turnV6) {
+            // Cache value to avoid a delay when starting up Jami
+            std::ofstream turnV6File(pathV6);
+            turnV6File << turnV6.toString();
+        } else
+            fileutils::remove(pathV6, true);
+        // Update TURN
+        testV6 = IpAddr(std::move(turnV6));
+    }
+    if (testV4)
+        testTurn(testV4);
+    if (testV6)
+        testTurn(testV6);
+
+    refreshTurnDelay(!testV4 && !testV6);
+}
+
+void
+TurnCache::testTurn(IpAddr server)
+{
+    TurnTransportParams params = params_;
+    params.server = server;
+    std::lock_guard<std::mutex> lk(cachedTurnMutex_);
+    auto& turn = server.isIpv4() ? testTurnV4_ : testTurnV6_;
+    turn.reset(); // Stop previous TURN
+    try {
+        turn = std::make_unique<TurnTransport>(
+            params, std::move([this, server](bool ok) {
+                std::lock_guard<std::mutex> lk(cachedTurnMutex_);
+                auto& cacheTurn = server.isIpv4() ? cacheTurnV4_ : cacheTurnV6_;
+                if (!ok) {
+                    JAMI_ERROR("Connection to {:s} failed - reset", server.toString());
+                    cacheTurn.reset();
+                } else {
+                    JAMI_DEBUG("Connection to {:s} ready", server.toString());
+                    cacheTurn = std::make_unique<IpAddr>(server);
+                }
+                refreshTurnDelay(!cacheTurnV6_ && !cacheTurnV4_);
+                auto& turn = server.isIpv4() ? testTurnV4_ : testTurnV6_;
+                turn->shutdown();
+            }));
+    } catch (const std::exception& e) {
+        JAMI_ERROR("TurnTransport creation error: {}", e.what());
+    }
+}
+
+void
+TurnCache::refreshTurnDelay(bool scheduleNext)
+{
+    isRefreshing_ = false;
+    if (scheduleNext) {
+        JAMI_WARNING("[Account {:s}] Cache for TURN resolution failed.", accountId_);
+        refreshTimer_->expires_at(std::chrono::steady_clock::now() + turnRefreshDelay_);
+        refreshTimer_->async_wait(std::bind(&TurnCache::refresh, this, std::placeholders::_1));
+        if (turnRefreshDelay_ < std::chrono::minutes(30))
+            turnRefreshDelay_ *= 2;
+    } else {
+        JAMI_DEBUG("[Account {:s}] Cache refreshed for TURN resolution", accountId_);
+        turnRefreshDelay_ = std::chrono::seconds(10);
+    }
+}
+
+} // namespace jami
diff --git a/src/connectivity/turn_cache.h b/src/connectivity/turn_cache.h
new file mode 100644
index 0000000000000000000000000000000000000000..967ede7abb8dabd42318af168889d80912ab10d0
--- /dev/null
+++ b/src/connectivity/turn_cache.h
@@ -0,0 +1,88 @@
+/*
+ *  Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#pragma once
+
+#include "connectivity/ip_utils.h"
+#include "connectivity/turn_transport.h"
+
+#include <atomic>
+#include <asio.hpp>
+#include <chrono>
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <string>
+
+namespace jami {
+
+class TurnCache
+{
+public:
+    TurnCache(const std::string& accountId,
+              const std::string& cachePath,
+              const TurnTransportParams& params,
+              bool enabled);
+    ~TurnCache();
+
+    std::optional<IpAddr> getResolvedTurn(uint16_t family = AF_INET) const;
+    /**
+     * Pass a new configuration for the cache
+     * @param param     The new configuration
+     */
+    void reconfigure(const TurnTransportParams& params, bool enabled);
+    /**
+     * Refresh cache from current configuration
+     */
+    void refresh(const asio::error_code& ec = {});
+
+private:
+    std::string accountId_;
+    std::string cachePath_;
+    TurnTransportParams params_;
+    std::atomic_bool enabled_ {false};
+    /**
+     * Avoid to refresh the cache multiple times
+     */
+    std::atomic_bool isRefreshing_ {false};
+    /**
+     * This will cache the turn server resolution each time we launch
+     * Jami, or for each connectivityChange()
+     */
+    void testTurn(IpAddr server);
+    std::unique_ptr<TurnTransport> testTurnV4_;
+    std::unique_ptr<TurnTransport> testTurnV6_;
+
+    // Used to detect if a turn server is down.
+    void refreshTurnDelay(bool scheduleNext);
+    std::chrono::seconds turnRefreshDelay_ {std::chrono::seconds(10)};
+
+    // Store resoved turn addresses
+    mutable std::mutex cachedTurnMutex_ {};
+    std::unique_ptr<IpAddr> cacheTurnV4_ {};
+    std::unique_ptr<IpAddr> cacheTurnV6_ {};
+
+    // io
+    std::shared_ptr<asio::io_context> io_context;
+    std::unique_ptr<asio::steady_timer> refreshTimer_;
+};
+
+} // namespace jami
diff --git a/src/connectivity/turn_transport.cpp b/src/connectivity/turn_transport.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5675a6c5d6e292f32ef6f48e6cbdf19e3831e256
--- /dev/null
+++ b/src/connectivity/turn_transport.cpp
@@ -0,0 +1,183 @@
+/*
+ *  Copyright (C) 2004-2022 Savoir-faire Linux Inc.
+ *
+ *  Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
+ *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#include "connectivity/turn_transport.h"
+#include "connectivity/sip_utils.h"
+#include "manager.h"
+
+#include <atomic>
+#include <thread>
+
+#include <pjnath.h>
+#include <pjlib-util.h>
+#include <pjlib.h>
+
+#define TRY(ret) \
+    do { \
+        if ((ret) != PJ_SUCCESS) \
+            throw std::runtime_error(#ret " failed"); \
+    } while (0)
+
+namespace jami {
+
+class TurnTransport::Impl
+{
+public:
+    Impl(std::function<void(bool)>&& cb) { cb_ = std::move(cb); }
+    ~Impl();
+
+    /**
+     * Detect new TURN state
+     */
+    void onTurnState(pj_turn_state_t old_state, pj_turn_state_t new_state);
+
+    /**
+     * Pool events from pjsip
+     */
+    void ioJob();
+
+    void start()
+    {
+        ioWorker = std::thread([this] { ioJob(); });
+    }
+
+    void stop() { stopped_ = true; }
+
+    TurnTransportParams settings;
+
+    pj_caching_pool poolCache {};
+    pj_pool_t* pool {nullptr};
+    pj_stun_config stunConfig {};
+    pj_turn_sock* relay {nullptr};
+    pj_str_t relayAddr {};
+    IpAddr peerRelayAddr; // address where peers should connect to
+    IpAddr mappedAddr;
+    std::function<void(bool)> cb_;
+
+    std::thread ioWorker;
+    std::atomic_bool stopped_ {false};
+};
+
+TurnTransport::Impl::~Impl()
+{
+    stop();
+    if (ioWorker.joinable())
+        ioWorker.join();
+    pj_caching_pool_destroy(&poolCache);
+}
+void
+TurnTransport::Impl::onTurnState(pj_turn_state_t old_state, pj_turn_state_t new_state)
+{
+    if (new_state == PJ_TURN_STATE_READY) {
+        pj_turn_session_info info;
+        pj_turn_sock_get_info(relay, &info);
+        peerRelayAddr = IpAddr {info.relay_addr};
+        mappedAddr = IpAddr {info.mapped_addr};
+        JAMI_DEBUG("TURN server ready, peer relay address: {:s}",
+                   peerRelayAddr.toString(true, true).c_str());
+        cb_(true);
+    } else if (old_state <= PJ_TURN_STATE_READY and new_state > PJ_TURN_STATE_READY) {
+        JAMI_WARNING("TURN server disconnected ({:s})", pj_turn_state_name(new_state));
+        cb_(false);
+    }
+}
+void
+TurnTransport::Impl::ioJob()
+{
+    const pj_time_val delay = {0, 10};
+    while (!stopped_) {
+        pj_ioqueue_poll(stunConfig.ioqueue, &delay);
+        pj_timer_heap_poll(stunConfig.timer_heap, nullptr);
+    }
+}
+
+TurnTransport::TurnTransport(const TurnTransportParams& params, std::function<void(bool)>&& cb)
+    : pimpl_ {new Impl(std::move(cb))}
+{
+    auto server = params.server;
+    if (!server.getPort())
+        server.setPort(PJ_STUN_PORT);
+    if (server.isUnspecified())
+        throw std::invalid_argument("invalid turn server address");
+    pimpl_->settings = params;
+    // PJSIP memory pool
+    pj_caching_pool_init(&pimpl_->poolCache, &pj_pool_factory_default_policy, 0);
+    pimpl_->pool = pj_pool_create(&pimpl_->poolCache.factory, "TurnTransport", 512, 512, nullptr);
+    if (not pimpl_->pool)
+        throw std::runtime_error("pj_pool_create() failed");
+    // STUN config
+    pj_stun_config_init(&pimpl_->stunConfig, &pimpl_->poolCache.factory, 0, nullptr, nullptr);
+    // create global timer heap
+    TRY(pj_timer_heap_create(pimpl_->pool, 1000, &pimpl_->stunConfig.timer_heap));
+    // create global ioqueue
+    TRY(pj_ioqueue_create(pimpl_->pool, 16, &pimpl_->stunConfig.ioqueue));
+    // TURN callbacks
+    pj_turn_sock_cb relay_cb;
+    pj_bzero(&relay_cb, sizeof(relay_cb));
+    relay_cb.on_state =
+        [](pj_turn_sock* relay, pj_turn_state_t old_state, pj_turn_state_t new_state) {
+            auto pimpl = static_cast<Impl*>(pj_turn_sock_get_user_data(relay));
+            pimpl->onTurnState(old_state, new_state);
+        };
+    // TURN socket config
+    pj_turn_sock_cfg turn_sock_cfg;
+    pj_turn_sock_cfg_default(&turn_sock_cfg);
+    turn_sock_cfg.max_pkt_size = 4096;
+    // TURN socket creation
+    TRY(pj_turn_sock_create(&pimpl_->stunConfig,
+                            server.getFamily(),
+                            PJ_TURN_TP_TCP,
+                            &relay_cb,
+                            &turn_sock_cfg,
+                            &*this->pimpl_,
+                            &pimpl_->relay));
+    // TURN allocation setup
+    pj_turn_alloc_param turn_alloc_param;
+    pj_turn_alloc_param_default(&turn_alloc_param);
+    turn_alloc_param.peer_conn_type = PJ_TURN_TP_TCP;
+    pj_stun_auth_cred cred;
+    pj_bzero(&cred, sizeof(cred));
+    cred.type = PJ_STUN_AUTH_CRED_STATIC;
+    pj_cstr(&cred.data.static_cred.realm, pimpl_->settings.realm.c_str());
+    pj_cstr(&cred.data.static_cred.username, pimpl_->settings.username.c_str());
+    cred.data.static_cred.data_type = PJ_STUN_PASSWD_PLAIN;
+    pj_cstr(&cred.data.static_cred.data, pimpl_->settings.password.c_str());
+    pimpl_->relayAddr = pj_strdup3(pimpl_->pool, server.toString().c_str());
+    // TURN connection/allocation
+    JAMI_DEBUG("Connecting to TURN {:s}", server.toString(true, true));
+    TRY(pj_turn_sock_alloc(pimpl_->relay,
+                           &pimpl_->relayAddr,
+                           server.getPort(),
+                           nullptr,
+                           &cred,
+                           &turn_alloc_param));
+    pimpl_->start();
+}
+
+TurnTransport::~TurnTransport() {}
+
+void
+TurnTransport::shutdown()
+{
+    pimpl_->stop();
+}
+
+} // namespace jami
diff --git a/src/connectivity/turn_transport.h b/src/connectivity/turn_transport.h
new file mode 100644
index 0000000000000000000000000000000000000000..5602221291fca9d0847fd04fb16bca2e62ec9f0d
--- /dev/null
+++ b/src/connectivity/turn_transport.h
@@ -0,0 +1,59 @@
+/*
+ *  Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ *  Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
+ *  Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#pragma once
+
+#include "connectivity/ip_utils.h"
+
+#include <functional>
+#include <memory>
+#include <string>
+
+namespace jami {
+
+struct TurnTransportParams
+{
+    IpAddr server;
+    std::string domain; // Used by cache_turn
+    // Plain Credentials
+    std::string realm;
+    std::string username;
+    std::string password;
+};
+
+/**
+ * This class is used to test connection to TURN servers
+ * No other logic is implemented.
+ */
+class TurnTransport
+{
+public:
+    TurnTransport(const TurnTransportParams& param, std::function<void(bool)>&& cb);
+    ~TurnTransport();
+    void shutdown();
+
+private:
+    TurnTransport() = delete;
+    class Impl;
+    std::unique_ptr<Impl> pimpl_;
+};
+
+} // namespace jami
diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp
index ee003ccfeb8b10a6a590e7732ce1c72c341a046a..7c3d0e06c0a10b53dcbf8d44af6b4ecb01989d4c 100644
--- a/src/jamidht/jamiaccount.cpp
+++ b/src/jamidht/jamiaccount.cpp
@@ -290,8 +290,7 @@ JamiAccount::JamiAccount(const std::string& accountId)
     , dataPath_(cachePath_ + DIR_SEPARATOR_STR "values")
     , connectionManager_ {}
     , nonSwarmTransferManager_(std::make_shared<TransferManager>(accountId, ""))
-{
-}
+{}
 
 JamiAccount::~JamiAccount() noexcept
 {
@@ -902,7 +901,7 @@ JamiAccount::loadConfig()
     registeredName_ = config().registeredName;
     try {
         auto str = fileutils::loadCacheTextFile(cachePath_ + DIR_SEPARATOR_STR "dhtproxy",
-                                                           std::chrono::hours(24 * 7));
+                                                std::chrono::hours(24 * 7));
         std::string err;
         Json::Value root;
         Json::CharReaderBuilder rbuilder;
@@ -1965,11 +1964,14 @@ JamiAccount::doRegister_()
                           getAccountID().c_str(),
                           name.c_str());
 
-                if (this->config().turnEnabled && !cacheTurnV4_) {
-                    // If TURN is enabled, but no TURN cached, there can be a temporary resolution
-                    // error to solve. Sometimes, a connectivity change is not enough, so even if
-                    // this case is really rare, it should be easy to avoid.
-                    cacheTurnServers();
+                if (this->config().turnEnabled && turnCache_) {
+                    auto addr = turnCache_->getResolvedTurn();
+                    if (addr == std::nullopt) {
+                        // If TURN is enabled, but no TURN cached, there can be a temporary
+                        // resolution error to solve. Sometimes, a connectivity change is not
+                        // enough, so even if this case is really rare, it should be easy to avoid.
+                        turnCache_->refresh();
+                    }
                 }
 
                 auto uri = Uri(name);
@@ -2278,7 +2280,7 @@ JamiAccount::setRegistrationState(RegistrationState state,
     if (registrationState_ != state) {
         if (state == RegistrationState::REGISTERED) {
             JAMI_WARN("[Account %s] connected", getAccountID().c_str());
-            cacheTurnServers();
+            turnCache_->refresh();
             storeActiveIpAddress();
         } else if (state == RegistrationState::TRYING) {
             JAMI_WARN("[Account %s] connecting…", getAccountID().c_str());
@@ -2342,11 +2344,10 @@ JamiAccount::setCertificateStatus(const std::string& cert_id,
 {
     bool done = accountManager_ ? accountManager_->setCertificateStatus(cert_id, status) : false;
     if (done) {
-        findCertificate(cert_id);
-        emitSignal<libjami::ConfigurationSignal::CertificateStateChanged>(getAccountID(),
-                                                                        cert_id,
-                                                                        tls::TrustStore::statusToStr(
-                                                                            status));
+        dht_->findCertificate(dht::InfoHash(cert_id),
+                              [](const std::shared_ptr<dht::crypto::Certificate>& crt) {});
+        emitSignal<libjami::ConfigurationSignal::CertificateStateChanged>(
+            getAccountID(), cert_id, tls::TrustStore::statusToStr(status));
     }
     return done;
 }
@@ -3532,106 +3533,6 @@ JamiAccount::handleMessage(const std::string& from, const std::pair<std::string,
     return false;
 }
 
-void
-JamiAccount::cacheTurnServers()
-{
-    // The resolution of the TURN server can take quite some time (if timeout).
-    // So, run this in its own io thread to avoid to block the main thread.
-    dht::ThreadPool::io().run([w = weak()] {
-        auto this_ = w.lock();
-        if (not this_)
-            return;
-        // Avoid multiple refresh
-        if (this_->isRefreshing_.exchange(true))
-            return;
-        const auto& conf = this_->config();
-        if (!conf.turnEnabled) {
-            // In this case, we do not use any TURN server
-            std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_);
-            this_->cacheTurnV4_.reset();
-            this_->cacheTurnV6_.reset();
-            this_->isRefreshing_ = false;
-            return;
-        }
-        JAMI_INFO("[Account %s] Refresh cache for TURN server resolution",
-                  this_->getAccountID().c_str());
-        // Retrieve old cached value if available.
-        // This means that we directly get the correct value when launching the application on the
-        // same network
-        std::string server = conf.turnServer.empty() ? DEFAULT_TURN_SERVER : conf.turnServer;
-        // No need to resolve, it's already a valid address
-        if (IpAddr::isValid(server, AF_INET)) {
-            this_->cacheTurnV4_ = std::make_unique<IpAddr>(server, AF_INET);
-            this_->isRefreshing_ = false;
-            return;
-        } else if (IpAddr::isValid(server, AF_INET6)) {
-            this_->cacheTurnV6_ = std::make_unique<IpAddr>(server, AF_INET6);
-            this_->isRefreshing_ = false;
-            return;
-        }
-        // Else cache resolution result
-        fileutils::recursive_mkdir(this_->cachePath_ + DIR_SEPARATOR_STR + "domains", 0700);
-        auto pathV4 = this_->cachePath_ + DIR_SEPARATOR_STR + "domains" + DIR_SEPARATOR_STR + "v4."
-                      + server;
-        if (auto turnV4File = std::ifstream(pathV4)) {
-            std::string content((std::istreambuf_iterator<char>(turnV4File)),
-                                std::istreambuf_iterator<char>());
-            std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_);
-            this_->cacheTurnV4_ = std::make_unique<IpAddr>(content, AF_INET);
-        }
-        auto pathV6 = this_->cachePath_ + DIR_SEPARATOR_STR + "domains" + DIR_SEPARATOR_STR + "v6."
-                      + server;
-        if (auto turnV6File = std::ifstream(pathV6)) {
-            std::string content((std::istreambuf_iterator<char>(turnV6File)),
-                                std::istreambuf_iterator<char>());
-            std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_);
-            this_->cacheTurnV6_ = std::make_unique<IpAddr>(content, AF_INET6);
-        }
-        // Resolve just in case. The user can have a different connectivity
-        auto turnV4 = IpAddr {server, AF_INET};
-        {
-            if (turnV4) {
-                // Cache value to avoid a delay when starting up Jami
-                std::ofstream turnV4File(pathV4);
-                turnV4File << turnV4.toString();
-            } else
-                fileutils::remove(pathV4, true);
-            std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_);
-            // Update TURN
-            this_->cacheTurnV4_ = std::make_unique<IpAddr>(std::move(turnV4));
-        }
-        auto turnV6 = IpAddr {server, AF_INET6};
-        {
-            if (turnV6) {
-                // Cache value to avoid a delay when starting up Jami
-                std::ofstream turnV6File(pathV6);
-                turnV6File << turnV6.toString();
-            } else
-                fileutils::remove(pathV6, true);
-            std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_);
-            // Update TURN
-            this_->cacheTurnV6_ = std::make_unique<IpAddr>(std::move(turnV6));
-        }
-        this_->isRefreshing_ = false;
-        if (!this_->cacheTurnV6_ && !this_->cacheTurnV4_) {
-            JAMI_WARN("[Account %s] Cache for TURN resolution failed.",
-                      this_->getAccountID().c_str());
-            Manager::instance().scheduleTaskIn(
-                [w]() {
-                    if (auto shared = w.lock())
-                        shared->cacheTurnServers();
-                },
-                this_->turnRefreshDelay_);
-            if (this_->turnRefreshDelay_ < std::chrono::minutes(30))
-                this_->turnRefreshDelay_ *= 2;
-        } else {
-            JAMI_INFO("[Account %s] Cache refreshed for TURN resolution",
-                      this_->getAccountID().c_str());
-            this_->turnRefreshDelay_ = std::chrono::seconds(10);
-        }
-    });
-}
-
 void
 JamiAccount::callConnectionClosed(const DeviceId& deviceId, bool eraseDummy)
 {
diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h
index 7c68f9f82ee815eae2a2bdd089c906b145c74638..72761daac1a71a3bef7f232c85f75a099023d16e 100644
--- a/src/jamidht/jamiaccount.h
+++ b/src/jamidht/jamiaccount.h
@@ -36,6 +36,7 @@
 #include "data_transfer.h"
 #include "uri.h"
 #include "jamiaccount_config.h"
+#include "connectivity/peer_connection.h"
 
 #include "noncopyable.h"
 #include "connectivity/ip_utils.h"
@@ -611,7 +612,8 @@ private:
     struct BuddyInfo;
     struct DiscoveredPeer;
 
-    inline std::string getProxyConfigKey() const {
+    inline std::string getProxyConfigKey() const
+    {
         const auto& conf = config();
         return dht::InfoHash::get(conf.proxyServer + conf.proxyListUrl).toString();
     }
@@ -785,7 +787,12 @@ private:
      * This will cache the turn server resolution each time we launch
      * Jami, or for each connectivityChange()
      */
+    // TODO move in separate class
+    void testTurn(IpAddr server);
     void cacheTurnServers();
+    std::unique_ptr<TurnTransport> testTurnV4_;
+    std::unique_ptr<TurnTransport> testTurnV6_;
+    void refreshTurnDelay(bool scheduleNext);
 
     std::chrono::seconds turnRefreshDelay_ {std::chrono::seconds(10)};
 
diff --git a/src/meson.build b/src/meson.build
index 06da38ed7c12ddf3805893e7127e308b2e4e7999..b0b5b08d658b3c67c36f02e033104a38f74464fb 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -29,6 +29,8 @@ libjami_sources = files(
     'connectivity/multiplexed_socket.cpp',
     'connectivity/peer_connection.cpp',
     'connectivity/sip_utils.cpp',
+    'connectivity/turn_cache.cpp',
+    'connectivity/turn_transport.cpp',
     'connectivity/utf8_utils.cpp',
     'im/instant_messaging.cpp',
     'im/message_engine.cpp',
diff --git a/src/sip/sipaccountbase.cpp b/src/sip/sipaccountbase.cpp
index 32c9aa56769d997d31f5569d78ee2861ad9b664a..1ecf03a0af7d46c20e420dbd54ebb257d11bd0a2 100644
--- a/src/sip/sipaccountbase.cpp
+++ b/src/sip/sipaccountbase.cpp
@@ -143,6 +143,20 @@ SIPAccountBase::loadConfig()
     IpAddr publishedIp {conf.publishedIp};
     if (not conf.publishedSameasLocal and publishedIp)
         setPublishedAddress(publishedIp);
+    TurnTransportParams turnParams;
+    turnParams.domain = conf.turnServer;
+    turnParams.username = conf.turnServerUserName;
+    turnParams.password = conf.turnServerPwd;
+    turnParams.realm = conf.turnServerRealm;
+    if (!turnCache_) {
+        auto cachePath = fileutils::get_cache_dir() + DIR_SEPARATOR_STR + getAccountID();
+        turnCache_ = std::make_unique<TurnCache>(getAccountID(),
+                                                 cachePath,
+                                                 turnParams,
+                                                 conf.turnEnabled);
+    } else {
+        turnCache_->reconfigure(turnParams, conf.turnEnabled);
+    }
 }
 
 std::map<std::string, std::string>
@@ -241,13 +255,11 @@ SIPAccountBase::getIceOptions() const noexcept
 
     // if (config().stunEnabled)
     //     opts.stunServers.emplace_back(StunServerInfo().setUri(stunServer_));
-    if (config().turnEnabled) {
-        auto cached = false;
-        std::lock_guard<std::mutex> lk(cachedTurnMutex_);
-        cached = cacheTurnV4_ || cacheTurnV6_;
-        if (cacheTurnV4_ && *cacheTurnV4_) {
+    if (config().turnEnabled && turnCache_) {
+        auto turnAddr = turnCache_->getResolvedTurn();
+        if (turnAddr != std::nullopt) {
             opts.turnServers.emplace_back(TurnServerInfo()
-                                              .setUri(cacheTurnV4_->toString(true))
+                                              .setUri(turnAddr->toString(true))
                                               .setUsername(config().turnServerUserName)
                                               .setPassword(config().turnServerPwd)
                                               .setRealm(config().turnServerRealm));
@@ -261,14 +273,6 @@ SIPAccountBase::getIceOptions() const noexcept
         //                                      .setPassword(turnServerPwd_)
         //                                      .setRealm(turnServerRealm_));
         //}
-        // Nothing cached, so do the resolution
-        if (!cached) {
-            opts.turnServers.emplace_back(TurnServerInfo()
-                                              .setUri(config().turnServer)
-                                              .setUsername(config().turnServerUserName)
-                                              .setPassword(config().turnServerPwd)
-                                              .setRealm(config().turnServerRealm));
-        }
     }
     return opts;
 }
diff --git a/src/sip/sipaccountbase.h b/src/sip/sipaccountbase.h
index a660d8266400e11f30886f2a90f284bb81fc39a4..70abe43ac5189dee7b6d467ac1cfce68e8ff7248 100644
--- a/src/sip/sipaccountbase.h
+++ b/src/sip/sipaccountbase.h
@@ -28,6 +28,7 @@
 
 #include "connectivity/sip_utils.h"
 #include "connectivity/ip_utils.h"
+#include "connectivity/turn_cache.h"
 #include "noncopyable.h"
 #include "im/message_engine.h"
 #include "sipaccountbase_config.h"
@@ -85,7 +86,8 @@ public:
 
     virtual ~SIPAccountBase() noexcept;
 
-    const SipAccountBaseConfig& config() const {
+    const SipAccountBaseConfig& config() const
+    {
         return *static_cast<const SipAccountBaseConfig*>(&Account::config());
     }
 
@@ -216,21 +218,6 @@ public:
 public: // overloaded methods
     virtual void flush() override;
 
-    /**
-     * Return current turn resolved addresses
-     * @return {unique_ptr(v4 resolved), unique_ptr(v6 resolved)}
-     */
-    std::array<std::unique_ptr<IpAddr>, 2> turnCache()
-    {
-        std::lock_guard<std::mutex> lk {cachedTurnMutex_};
-        std::array<std::unique_ptr<IpAddr>, 2> result = {};
-        if (cacheTurnV4_ && *cacheTurnV4_)
-            result[0] = std::make_unique<IpAddr>(*cacheTurnV4_);
-        if (cacheTurnV6_ && *cacheTurnV6_)
-            result[1] = std::make_unique<IpAddr>(*cacheTurnV6_);
-        return result;
-    }
-
 protected:
     /**
      * Retrieve volatile details such as recent registration errors
@@ -279,9 +266,7 @@ protected:
         std::chrono::steady_clock::time_point::min()};
     std::shared_ptr<Task> composingTimeout_;
 
-    mutable std::mutex cachedTurnMutex_ {};
-    std::unique_ptr<IpAddr> cacheTurnV4_ {};
-    std::unique_ptr<IpAddr> cacheTurnV6_ {};
+    std::unique_ptr<TurnCache> turnCache_;
 
 private:
     NON_COPYABLE(SIPAccountBase);
diff --git a/test/unitTest/call/call.cpp b/test/unitTest/call/call.cpp
index 4e6f24965d4c93a434dbe30e3e0295b98901795e..a52088dc6b24cfc5a26ee191bd0049b61fa8ba7e 100644
--- a/test/unitTest/call/call.cpp
+++ b/test/unitTest/call/call.cpp
@@ -38,7 +38,7 @@
 
 using namespace libjami::Account;
 using namespace libjami::Call::Details;
-
+using namespace std::literals::chrono_literals;
 namespace jami {
 namespace test {
 
@@ -48,7 +48,8 @@ public:
     CallTest()
     {
         // Init daemon
-        libjami::init(libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG));
+        libjami::init(
+            libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG));
         if (not Manager::instance().initialized)
             CPPUNIT_ASSERT(libjami::start("jami-sample.yml"));
     }
@@ -68,6 +69,7 @@ private:
     void testDeclineMultiDevice();
     void testTlsInfosPeerCertificate();
     void testSocketInfos();
+    void testInvalidTurn();
 
     CPPUNIT_TEST_SUITE(CallTest);
     CPPUNIT_TEST(testCall);
@@ -76,6 +78,7 @@ private:
     CPPUNIT_TEST(testDeclineMultiDevice);
     CPPUNIT_TEST(testTlsInfosPeerCertificate);
     CPPUNIT_TEST(testSocketInfos);
+    CPPUNIT_TEST(testInvalidTurn);
     CPPUNIT_TEST_SUITE_END();
 };
 
@@ -138,12 +141,12 @@ CallTest::testCall()
     JAMI_INFO("Start call between alice and Bob");
     auto call = libjami::placeCallWithMedia(aliceId, bobUri, {});
 
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return callReceived.load(); }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callReceived.load(); }));
 
     JAMI_INFO("Stop call between alice and Bob");
     callStopped = 0;
     Manager::instance().hangupCall(aliceId, call);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return callStopped == 2; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callStopped == 2; }));
 }
 
 void
@@ -190,17 +193,16 @@ CallTest::testCachedCall()
                                successfullyConnected = true;
                            cv.notify_one();
                        });
-    CPPUNIT_ASSERT(
-        cv.wait_for(lk, std::chrono::seconds(30), [&] { return successfullyConnected.load(); }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return successfullyConnected.load(); }));
 
     JAMI_INFO("Start call between alice and Bob");
     auto call = libjami::placeCallWithMedia(aliceId, bobUri, {});
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return callReceived.load(); }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callReceived.load(); }));
 
     callStopped = 0;
     JAMI_INFO("Stop call between alice and Bob");
     Manager::instance().hangupCall(aliceId, call);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return callStopped == 2; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callStopped == 2; }));
 }
 
 void
@@ -292,14 +294,12 @@ CallTest::testDeclineMultiDevice()
     JAMI_INFO("Start call between alice and Bob");
     auto call = libjami::placeCallWithMedia(aliceId, bobUri, {});
 
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] {
-        return callReceived == 2 && !callIdBob.empty();
-    }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callReceived == 2 && !callIdBob.empty(); }));
 
     JAMI_INFO("Stop call between alice and Bob");
     callStopped = 0;
     Manager::instance().refuseCall(bobId, callIdBob);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] {
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] {
         return callStopped.load() >= 3; /* >= because there is subcalls */
     }));
 }
@@ -344,11 +344,10 @@ CallTest::testTlsInfosPeerCertificate()
     JAMI_INFO("Start call between alice and Bob");
     auto callId = libjami::placeCallWithMedia(aliceId, bobUri, {});
 
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return !bobCallId.empty(); }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return !bobCallId.empty(); }));
 
     Manager::instance().answerCall(bobId, bobCallId);
-    CPPUNIT_ASSERT(
-        cv.wait_for(lk, std::chrono::seconds(30), [&] { return aliceCallState == "CURRENT"; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return aliceCallState == "CURRENT"; }));
 
     auto call = std::dynamic_pointer_cast<SIPCall>(aliceAccount->getCall(callId));
     auto* transport = call->getTransport();
@@ -360,7 +359,7 @@ CallTest::testTlsInfosPeerCertificate()
     JAMI_INFO("Stop call between alice and Bob");
     callStopped = 0;
     Manager::instance().hangupCall(aliceId, callId);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return callStopped == 2; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callStopped == 2; }));
 }
 
 void
@@ -413,12 +412,10 @@ CallTest::testSocketInfos()
     JAMI_INFO("Start call between alice and Bob");
     auto callId = libjami::placeCallWithMedia(aliceId, bobUri, {});
 
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return !bobCallId.empty(); }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return !bobCallId.empty(); }));
 
     Manager::instance().answerCall(bobId, bobCallId);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] {
-        return aliceCallState == "CURRENT" && mediaReady;
-    }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return aliceCallState == "CURRENT" && mediaReady; }));
 
     JAMI_INFO("Detail debug");
     auto details = libjami::getCallDetails(aliceId, callId);
@@ -434,7 +431,55 @@ CallTest::testSocketInfos()
     JAMI_INFO("Stop call between alice and Bob");
     callStopped = 0;
     Manager::instance().hangupCall(aliceId, callId);
-    CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&] { return callStopped == 2; }));
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callStopped == 2; }));
+}
+
+void
+CallTest::testInvalidTurn()
+{
+    auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId);
+    auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId);
+    auto bobUri = bobAccount->getUsername();
+    auto aliceUri = aliceAccount->getUsername();
+
+    std::mutex mtx;
+    std::unique_lock<std::mutex> lk {mtx};
+    std::condition_variable cv;
+    std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers;
+    std::atomic_bool callReceived {false};
+    std::atomic<int> callStopped {0};
+    // Watch signals
+    confHandlers.insert(libjami::exportable_callback<libjami::CallSignal::IncomingCallWithMedia>(
+        [&](const std::string&,
+            const std::string&,
+            const std::string&,
+            const std::vector<std::map<std::string, std::string>>&) {
+            callReceived = true;
+            cv.notify_one();
+        }));
+    confHandlers.insert(libjami::exportable_callback<libjami::CallSignal::StateChange>(
+        [&](const std::string&, const std::string&, const std::string& state, signed) {
+            if (state == "OVER") {
+                callStopped += 1;
+                if (callStopped == 2)
+                    cv.notify_one();
+            }
+        }));
+    libjami::registerSignalHandlers(confHandlers);
+
+    std::map<std::string, std::string> details;
+    details[ConfProperties::TURN::SERVER] = "1.1.1.1";
+    libjami::setAccountDetails(aliceId, details);
+
+    JAMI_INFO("Start call between alice and Bob");
+    auto call = libjami::placeCallWithMedia(aliceId, bobUri, {});
+
+    CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&] { return callReceived.load(); }));
+
+    JAMI_INFO("Stop call between alice and Bob");
+    callStopped = 0;
+    Manager::instance().hangupCall(aliceId, call);
+    CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return callStopped == 2; }));
 }
 
 } // namespace test