diff -ruw dnsdist-2.0.0.orig/dnsdist-doh-common.hh dnsdist-2.0.0/dnsdist-doh-common.hh
--- dnsdist-2.0.0.orig/dnsdist-doh-common.hh	2025-07-21 11:54:44.000000000 +0200
+++ dnsdist-2.0.0/dnsdist-doh-common.hh	2025-09-11 11:15:29.763483246 +0200
@@ -35,6 +35,8 @@
 
 namespace dnsdist::doh
 {
+static constexpr uint32_t MAX_INCOMING_CONCURRENT_STREAMS{100U};
+
 std::optional<PacketBuffer> getPayloadFromPath(const std::string_view& path);
 }
 
diff -ruw dnsdist-2.0.0.orig/dnsdist-nghttp2-in.cc dnsdist-2.0.0/dnsdist-nghttp2-in.cc
--- dnsdist-2.0.0.orig/dnsdist-nghttp2-in.cc	2025-07-21 11:54:44.000000000 +0200
+++ dnsdist-2.0.0/dnsdist-nghttp2-in.cc	2025-09-11 11:15:57.559364196 +0200
@@ -292,7 +292,7 @@
 
 void IncomingHTTP2Connection::handleConnectionReady()
 {
-  constexpr std::array<nghttp2_settings_entry, 1> settings{{{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100U}}};
+  constexpr std::array<nghttp2_settings_entry, 1> settings{{{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, dnsdist::doh::MAX_INCOMING_CONCURRENT_STREAMS}}};
   constexpr std::array<nghttp2_settings_entry, 1> nearLimitsSettings{{{NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 1U}}};
   auto ret = nghttp2_submit_settings(d_session.get(), NGHTTP2_FLAG_NONE, isNearTCPLimits() ? nearLimitsSettings.data() : settings.data(), isNearTCPLimits() ? nearLimitsSettings.size() : settings.size());
   if (ret != 0) {
@@ -443,6 +443,24 @@
       if (nghttp2_session_want_read(d_session.get()) != 0) {
         updateIO(IOState::NeedRead, handleReadableIOCallback);
       }
+      else {
+        if (getConcurrentStreamsCount() == 0) {
+          d_connectionDied = true;
+          stopIO();
+        }
+        else {
+          updateIO(IOState::Done, handleReadableIOCallback);
+        }
+      }
+    }
+    else {
+      if (getConcurrentStreamsCount() == 0) {
+        d_connectionDied = true;
+        stopIO();
+      }
+      else {
+        updateIO(IOState::Done, handleReadableIOCallback);
+      }
     }
   }
   catch (const std::exception& e) {
@@ -552,13 +570,23 @@
   NGHTTP2Headers::addCustomDynamicHeader(headers, name, value);
 }
 
+std::unordered_map<IncomingHTTP2Connection::StreamID, IncomingHTTP2Connection::PendingQuery>::iterator IncomingHTTP2Connection::getStreamContext(StreamID streamID)
+{
+  auto streamIt = d_currentStreams.find(streamID);
+  if (streamIt == d_currentStreams.end()) {
+    /* it might have been closed by the remote end in the meantime */
+    d_killedStreams.erase(streamID);
+  }
+  return streamIt;
+}
+
 IOState IncomingHTTP2Connection::sendResponse(const struct timeval& now, TCPResponse&& response)
 {
   (void)now;
   if (response.d_idstate.d_streamID == -1) {
     throw std::runtime_error("Invalid DoH stream ID while sending response");
   }
-  auto streamIt = d_currentStreams.find(response.d_idstate.d_streamID);
+  auto streamIt = getStreamContext(response.d_idstate.d_streamID);
   if (streamIt == d_currentStreams.end()) {
     /* it might have been closed by the remote end in the meantime */
     return hasPendingWrite() ? IOState::NeedWrite : IOState::Done;
@@ -598,7 +626,7 @@
     throw std::runtime_error("Invalid DoH stream ID while handling I/O error notification");
   }
 
-  auto streamIt = d_currentStreams.find(response.d_idstate.d_streamID);
+  auto streamIt = getStreamContext(response.d_idstate.d_streamID);
   if (streamIt == d_currentStreams.end()) {
     /* it might have been closed by the remote end in the meantime */
     return;
@@ -742,17 +770,18 @@
     NGHTTP2Headers::addCustomDynamicHeader(headers, key, value);
   }
 
+  context.d_sendingResponse = true;
   auto ret = nghttp2_submit_response(d_session.get(), streamID, headers.data(), headers.size(), &data_provider);
   if (ret != 0) {
-    d_currentStreams.erase(streamID);
     vinfolog("Error submitting HTTP response for stream %d: %s", streamID, nghttp2_strerror(ret));
+    d_currentStreams.erase(streamID);
     return false;
   }
 
   ret = nghttp2_session_send(d_session.get());
   if (ret != 0) {
-    d_currentStreams.erase(streamID);
     vinfolog("Error flushing HTTP response for stream %d: %s", streamID, nghttp2_strerror(ret));
+    d_currentStreams.erase(streamID);
     return false;
   }
 
@@ -926,7 +955,7 @@
   /* is this the last frame for this stream? */
   if ((frame->hd.type == NGHTTP2_HEADERS || frame->hd.type == NGHTTP2_DATA) && (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) != 0) {
     auto streamID = frame->hd.stream_id;
-    auto stream = conn->d_currentStreams.find(streamID);
+    auto stream = conn->getStreamContext(streamID);
     if (stream != conn->d_currentStreams.end()) {
       conn->handleIncomingQuery(std::move(stream->second), streamID);
     }
@@ -948,7 +977,16 @@
   (void)error_code;
   auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
 
-  conn->d_currentStreams.erase(stream_id);
+  auto streamIt = conn->d_currentStreams.find(stream_id);
+  if (streamIt == conn->d_currentStreams.end()) {
+    return 0;
+  }
+
+  if (!streamIt->second.d_sendingResponse) {
+    conn->d_killedStreams.emplace(stream_id);
+  }
+
+  conn->d_currentStreams.erase(streamIt);
   return 0;
 }
 
@@ -960,20 +998,29 @@
   }
 
   auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
-  auto insertPair = conn->d_currentStreams.emplace(frame->hd.stream_id, PendingQuery());
-  if (!insertPair.second) {
-    /* there is a stream ID collision, something is very wrong! */
-    vinfolog("Stream ID collision (%d) on connection from %d", frame->hd.stream_id, conn->d_ci.remote.toStringWithPort());
-    conn->d_connectionClosing = true;
-    conn->d_needFlush = true;
-    nghttp2_session_terminate_session(conn->d_session.get(), NGHTTP2_NO_ERROR);
-    auto ret = nghttp2_session_send(conn->d_session.get());
+  auto close_connection = [](IncomingHTTP2Connection* connection, int32_t streamID, const ComboAddress& remote) -> int {
+    connection->d_connectionClosing = true;
+    connection->d_needFlush = true;
+    nghttp2_session_terminate_session(connection->d_session.get(), NGHTTP2_REFUSED_STREAM);
+    auto ret = nghttp2_session_send(connection->d_session.get());
     if (ret != 0) {
-      vinfolog("Error flushing HTTP response for stream %d from %s: %s", frame->hd.stream_id, conn->d_ci.remote.toStringWithPort(), nghttp2_strerror(ret));
+      vinfolog("Error flushing HTTP response for stream %d from %s: %s", streamID, remote.toStringWithPort(), nghttp2_strerror(ret));
       return NGHTTP2_ERR_CALLBACK_FAILURE;
     }
 
     return 0;
+  };
+
+  if (conn->getConcurrentStreamsCount() >= dnsdist::doh::MAX_INCOMING_CONCURRENT_STREAMS) {
+    vinfolog("Too many concurrent streams on connection from %s", conn->d_ci.remote.toStringWithPort());
+    return close_connection(conn, frame->hd.stream_id, conn->d_ci.remote);
+  }
+
+  auto insertPair = conn->d_currentStreams.emplace(frame->hd.stream_id, PendingQuery());
+  if (!insertPair.second) {
+    /* there is a stream ID collision, something is very wrong! */
+    vinfolog("Stream ID collision (%d) on connection from %s", frame->hd.stream_id, conn->d_ci.remote.toStringWithPort());
+    return close_connection(conn, frame->hd.stream_id, conn->d_ci.remote);
   }
 
   return 0;
@@ -1012,7 +1059,7 @@
       return nameLen == expected.size() && memcmp(name, expected.data(), expected.size()) == 0;
     };
 
-    auto stream = conn->d_currentStreams.find(frame->hd.stream_id);
+    auto stream = conn->getStreamContext(frame->hd.stream_id);
     if (stream == conn->d_currentStreams.end()) {
       vinfolog("Unable to match the stream ID %d to a known one!", frame->hd.stream_id);
       return NGHTTP2_ERR_CALLBACK_FAILURE;
@@ -1077,7 +1124,7 @@
   (void)session;
   (void)flags;
   auto* conn = static_cast<IncomingHTTP2Connection*>(user_data);
-  auto stream = conn->d_currentStreams.find(stream_id);
+  auto stream = conn->getStreamContext(stream_id);
   if (stream == conn->d_currentStreams.end()) {
     vinfolog("Unable to match the stream ID %d to a known one!", stream_id);
     return NGHTTP2_ERR_CALLBACK_FAILURE;
@@ -1168,7 +1215,7 @@
 
 uint32_t IncomingHTTP2Connection::getConcurrentStreamsCount() const
 {
-  return d_currentStreams.size();
+  return d_currentStreams.size() + d_killedStreams.size();
 }
 
 boost::optional<struct timeval> IncomingHTTP2Connection::getIdleClientReadTTD(struct timeval now) const
@@ -1234,6 +1281,9 @@
     ttd = getClientWriteTTD(now);
     d_ioState->update(newState, callback, shared, ttd);
   }
+  else if (newState == IOState::Done) {
+    d_ioState->reset();
+  }
 }
 
 void IncomingHTTP2Connection::handleIOError()
@@ -1243,6 +1293,7 @@
   d_outPos = 0;
   nghttp2_session_terminate_session(d_session.get(), NGHTTP2_PROTOCOL_ERROR);
   d_currentStreams.clear();
+  d_killedStreams.clear();
   stopIO();
 }
 
diff -ruw dnsdist-2.0.0.orig/dnsdist-nghttp2-in.hh dnsdist-2.0.0/dnsdist-nghttp2-in.hh
--- dnsdist-2.0.0.orig/dnsdist-nghttp2-in.hh	2025-07-21 11:54:44.000000000 +0200
+++ dnsdist-2.0.0/dnsdist-nghttp2-in.hh	2025-09-11 11:15:35.865177127 +0200
@@ -55,6 +55,7 @@
     size_t d_queryPos{0};
     uint32_t d_statusCode{0};
     Method d_method{Method::Unknown};
+    bool d_sendingResponse{false};
   };
 
   IncomingHTTP2Connection(ConnectionInfo&& connectionInfo, TCPClientThreadData& threadData, const struct timeval& now);
@@ -86,6 +87,7 @@
   std::unique_ptr<DOHUnitInterface> getDOHUnit(uint32_t streamID) override;
 
   void stopIO();
+  std::unordered_map<StreamID, PendingQuery>::iterator getStreamContext(StreamID streamID);
   uint32_t getConcurrentStreamsCount() const;
   void updateIO(IOState newState, const timeval& now) override;
   void updateIO(IOState newState, const FDMultiplexer::callbackfunc_t& callback);
@@ -102,6 +104,7 @@
 
   std::unique_ptr<nghttp2_session, decltype(&nghttp2_session_del)> d_session{nullptr, nghttp2_session_del};
   std::unordered_map<StreamID, PendingQuery> d_currentStreams;
+  std::unordered_set<StreamID> d_killedStreams;
   PacketBuffer d_out;
   PacketBuffer d_in;
   size_t d_outPos{0};
diff -ruw dnsdist-2.0.0.orig/doh.cc dnsdist-2.0.0/doh.cc
--- dnsdist-2.0.0.orig/doh.cc	2025-07-21 11:54:44.000000000 +0200
+++ dnsdist-2.0.0/doh.cc	2025-09-11 11:15:49.102208839 +0200
@@ -316,6 +316,7 @@
   struct timeval d_connectionStartTime{0, 0};
   size_t d_nbQueries{0};
   int d_desc{-1};
+  uint8_t d_concurrentStreams{0};
 };
 
 static thread_local std::unordered_map<int, DOHConnection> t_conns;
@@ -389,6 +390,17 @@
   return reasonIt->second;
 }
 
+static DOHConnection* getConnectionFromQuery(const h2o_req_t* req)
+{
+  h2o_socket_t* sock = req->conn->callbacks->get_socket(req->conn);
+  const int descriptor = h2o_socket_get_fd(sock);
+  if (descriptor == -1) {
+    /* this should not happen, but let's not crash on it */
+    return nullptr;
+  }
+  return &t_conns.at(descriptor);
+}
+
 /* Always called from the main DoH thread */
 static void handleResponse(DOHFrontend& dohFrontend, st_h2o_req_t* req, uint16_t statusCode, const PacketBuffer& response, const std::unordered_map<std::string, std::string>& customResponseHeaders, const std::string& contentType, bool addContentType)
 {
@@ -464,6 +476,10 @@
 
     ++dohFrontend.d_errorresponses;
   }
+
+  if (auto* conn = getConnectionFromQuery(req)) {
+    --conn->d_concurrentStreams;
+  }
 }
 
 static std::unique_ptr<DOHUnit> getDUFromIDS(InternalQueryState& ids)
@@ -920,6 +936,8 @@
    via a pipe */
 static void doh_dispatch_query(DOHServerConfig* dsc, h2o_handler_t* self, h2o_req_t* req, PacketBuffer&& query, const ComboAddress& local, const ComboAddress& remote, std::string&& path)
 {
+  auto* conn = getConnectionFromQuery(req);
+
   try {
     /* we only parse it there as a sanity check, we will parse it again later */
     // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
@@ -951,6 +969,9 @@
       }
     }
 
+    if (conn != nullptr) {
+      ++conn->d_concurrentStreams;
+    }
 #ifdef HAVE_H2O_SOCKET_GET_SSL_SERVER_NAME
     h2o_socket_t* sock = req->conn->callbacks->get_socket(req->conn);
     const char * sni = h2o_socket_get_ssl_server_name(sock);
@@ -968,17 +989,26 @@
       if (!dsc->d_querySender.send(std::move(dohUnit))) {
         ++dnsdist::metrics::g_stats.dohQueryPipeFull;
         vinfolog("Unable to pass a DoH query to the DoH worker thread because the pipe is full");
+        if (conn != nullptr) {
+          --conn->d_concurrentStreams;
+        }
         h2o_send_error_500(req, "Internal Server Error", "Internal Server Error", 0);
       }
     }
     catch (...) {
       vinfolog("Unable to pass a DoH query to the DoH worker thread because we couldn't write to the pipe: %s", stringerror());
+      if (conn != nullptr) {
+        --conn->d_concurrentStreams;
+      }
       h2o_send_error_500(req, "Internal Server Error", "Internal Server Error", 0);
     }
 #endif /* USE_SINGLE_ACCEPTOR_THREAD */
   }
   catch (const std::exception& e) {
     vinfolog("Had error parsing DoH DNS packet from %s: %s", remote.toStringWithPort(), e.what());
+    if (conn != nullptr) {
+      --conn->d_concurrentStreams;
+    }
     h2o_send_error_400(req, "Bad Request", "The DNS query could not be parsed", 0);
   }
 }
@@ -1048,15 +1078,19 @@
     }
     // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic): h2o API
     auto* dsc = static_cast<DOHServerConfig*>(req->conn->ctx->storage.entries[0].data);
-    h2o_socket_t* sock = req->conn->callbacks->get_socket(req->conn);
-
-    const int descriptor = h2o_socket_get_fd(sock);
-    if (descriptor == -1) {
+    auto* connPtr = getConnectionFromQuery(req);
+    if (connPtr == nullptr) {
+      return 0;
+    }
+    auto& conn = *connPtr;
+    if (conn.d_concurrentStreams >= dnsdist::doh::MAX_INCOMING_CONCURRENT_STREAMS) {
+      vinfolog("Too many concurrent streams on connection from %d", conn.d_remote.toStringWithPort());
       return 0;
     }
 
-    auto& conn = t_conns.at(descriptor);
     ++conn.d_nbQueries;
+
+    h2o_socket_t* sock = req->conn->callbacks->get_socket(req->conn);
     if (conn.d_nbQueries == 1) {
       if (h2o_socket_get_ssl_session_reused(sock) == 0) {
         ++dsc->clientState->tlsNewSessions;
@@ -1122,6 +1156,7 @@
       for (const auto& entry : *responsesMap) {
         if (entry->matches(path)) {
           const auto& customHeaders = entry->getHeaders();
+          ++conn.d_concurrentStreams;
           handleResponse(*dsc->dohFrontend, req, entry->getStatusCode(), entry->getContent(), customHeaders ? *customHeaders : dsc->dohFrontend->d_customResponseHeaders, std::string(), false);
           return 0;
         }
