diff --git a/lib/ngtcp2_conn.c b/lib/ngtcp2_conn.c
index 4843a23849286c7b066aa1d6a48ded36ffb6a488..fe150e414878346b972cfc0b5e3b6f14a093c349 100644
--- a/lib/ngtcp2_conn.c
+++ b/lib/ngtcp2_conn.c
@@ -1366,8 +1366,7 @@ static size_t conn_cryptofrq_unacked_offset(ngtcp2_conn *conn,
                                             ngtcp2_pktns *pktns) {
   ngtcp2_frame_chain *frc;
   ngtcp2_crypto *fr;
-  ngtcp2_ksl_it gapit;
-  ngtcp2_range *gap;
+  ngtcp2_range gap;
   ngtcp2_rtb *rtb = &pktns->rtb;
   ngtcp2_ksl_it it;
   size_t datalen;
@@ -1379,17 +1378,15 @@ static size_t conn_cryptofrq_unacked_offset(ngtcp2_conn *conn,
     frc = ngtcp2_ksl_it_get(&it);
     fr = &frc->fr.crypto;
 
-    gapit = ngtcp2_gaptr_get_first_gap_after(&rtb->crypto->tx.acked_offset,
-                                             fr->offset);
-    gap = ngtcp2_ksl_it_key(&gapit);
+    gap = ngtcp2_strm_get_unacked_range_after(rtb->crypto, fr->offset);
 
     datalen = ngtcp2_vec_len(fr->data, fr->datacnt);
 
-    if (gap->begin <= fr->offset) {
+    if (gap.begin <= fr->offset) {
       return fr->offset;
     }
-    if (gap->begin < fr->offset + datalen) {
-      return gap->begin;
+    if (gap.begin < fr->offset + datalen) {
+      return gap.begin;
     }
   }
 
@@ -1403,7 +1400,6 @@ static int conn_cryptofrq_unacked_pop(ngtcp2_conn *conn, ngtcp2_pktns *pktns,
   uint64_t offset, end_offset;
   size_t idx, end_idx;
   uint64_t base_offset, end_base_offset;
-  ngtcp2_ksl_it gapit;
   ngtcp2_range gap;
   ngtcp2_rtb *rtb = &pktns->rtb;
   ngtcp2_vec *v;
@@ -1422,9 +1418,7 @@ static int conn_cryptofrq_unacked_pop(ngtcp2_conn *conn, ngtcp2_pktns *pktns,
     offset = fr->offset;
     base_offset = 0;
 
-    gapit =
-        ngtcp2_gaptr_get_first_gap_after(&rtb->crypto->tx.acked_offset, offset);
-    gap = *(ngtcp2_range *)ngtcp2_ksl_it_key(&gapit);
+    gap = ngtcp2_strm_get_unacked_range_after(rtb->crypto, offset);
     if (gap.begin < offset) {
       gap.begin = offset;
     }
diff --git a/lib/ngtcp2_rtb.c b/lib/ngtcp2_rtb.c
index e7e6762e03daf4b0081f44c88d59c0b3f40f4f8b..926258d900e5e09acd196625c3a632176919d4f6 100644
--- a/lib/ngtcp2_rtb.c
+++ b/lib/ngtcp2_rtb.c
@@ -289,17 +289,16 @@ static int rtb_process_acked_pkt(ngtcp2_rtb *rtb, ngtcp2_rtb_entry *ent,
       if (strm == NULL) {
         break;
       }
-      prev_stream_offset =
-          ngtcp2_gaptr_first_gap_offset(&strm->tx.acked_offset);
-      rv = ngtcp2_gaptr_push(
-          &strm->tx.acked_offset, frc->fr.stream.offset,
+      prev_stream_offset = ngtcp2_strm_get_acked_offset(strm);
+      rv = ngtcp2_strm_ack_data(
+          strm, frc->fr.stream.offset,
           ngtcp2_vec_len(frc->fr.stream.data, frc->fr.stream.datacnt));
       if (rv != 0) {
         return rv;
       }
 
       if (conn->callbacks.acked_stream_data_offset) {
-        stream_offset = ngtcp2_gaptr_first_gap_offset(&strm->tx.acked_offset);
+        stream_offset = ngtcp2_strm_get_acked_offset(strm);
         datalen = stream_offset - prev_stream_offset;
         if (datalen == 0 && !frc->fr.stream.fin) {
           break;
@@ -319,17 +318,16 @@ static int rtb_process_acked_pkt(ngtcp2_rtb *rtb, ngtcp2_rtb_entry *ent,
       }
       break;
     case NGTCP2_FRAME_CRYPTO:
-      prev_stream_offset =
-          ngtcp2_gaptr_first_gap_offset(&crypto->tx.acked_offset);
-      rv = ngtcp2_gaptr_push(
-          &crypto->tx.acked_offset, frc->fr.crypto.offset,
+      prev_stream_offset = ngtcp2_strm_get_acked_offset(crypto);
+      rv = ngtcp2_strm_ack_data(
+          crypto, frc->fr.crypto.offset,
           ngtcp2_vec_len(frc->fr.crypto.data, frc->fr.crypto.datacnt));
       if (rv != 0) {
         return rv;
       }
 
       if (conn->callbacks.acked_crypto_offset) {
-        stream_offset = ngtcp2_gaptr_first_gap_offset(&crypto->tx.acked_offset);
+        stream_offset = ngtcp2_strm_get_acked_offset(crypto);
         datalen = stream_offset - prev_stream_offset;
         if (datalen == 0) {
           break;
@@ -628,7 +626,6 @@ int ngtcp2_rtb_on_crypto_timeout(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
   ngtcp2_ksl_it it;
   ngtcp2_frame_chain *nfrc;
   ngtcp2_frame_chain *frc;
-  ngtcp2_ksl_it gapit;
   ngtcp2_range gap, range;
   ngtcp2_crypto *fr;
   int all_acked;
@@ -654,9 +651,7 @@ int ngtcp2_rtb_on_crypto_timeout(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
 
       /* Don't resend CRYPTO frame if the whole region it contains has
          been acknowledged */
-      gapit = ngtcp2_gaptr_get_first_gap_after(&rtb->crypto->tx.acked_offset,
-                                               fr->offset);
-      gap = *(ngtcp2_range *)ngtcp2_ksl_it_key(&gapit);
+      gap = ngtcp2_strm_get_unacked_range_after(rtb->crypto, fr->offset);
 
       range.begin = fr->offset;
       range.end = fr->offset + ngtcp2_vec_len(fr->data, fr->datacnt);
diff --git a/lib/ngtcp2_strm.c b/lib/ngtcp2_strm.c
index 4f88fb30337207cb98e59df224e012d262cc0a93..214683de9d31e30e1db9e5a66dd5f459b18d4bb9 100644
--- a/lib/ngtcp2_strm.c
+++ b/lib/ngtcp2_strm.c
@@ -39,6 +39,8 @@ int ngtcp2_strm_init(ngtcp2_strm *strm, int64_t stream_id, uint32_t flags,
                      uint64_t max_rx_offset, uint64_t max_tx_offset,
                      void *stream_user_data, const ngtcp2_mem *mem) {
   strm->cycle = 0;
+  strm->tx.acked_offset = NULL;
+  strm->tx.cont_acked_offset = 0;
   strm->tx.streamfrq = NULL;
   strm->tx.offset = 0;
   strm->tx.max_offset = max_tx_offset;
@@ -55,7 +57,7 @@ int ngtcp2_strm_init(ngtcp2_strm *strm, int64_t stream_id, uint32_t flags,
   strm->mem = mem;
   strm->app_error_code = 0;
 
-  return ngtcp2_gaptr_init(&strm->tx.acked_offset, mem);
+  return 0;
 }
 
 void ngtcp2_strm_free(ngtcp2_strm *strm) {
@@ -72,10 +74,13 @@ void ngtcp2_strm_free(ngtcp2_strm *strm) {
     }
 
     ngtcp2_ksl_free(strm->tx.streamfrq);
+    ngtcp2_mem_free(strm->mem, strm->tx.streamfrq);
   }
 
   ngtcp2_rob_free(strm->rx.rob);
-  ngtcp2_gaptr_free(&strm->tx.acked_offset);
+  ngtcp2_mem_free(strm->mem, strm->rx.rob);
+  ngtcp2_gaptr_free(strm->tx.acked_offset);
+  ngtcp2_mem_free(strm->mem, strm->tx.acked_offset);
 }
 
 static int strm_rob_init(ngtcp2_strm *strm) {
@@ -371,6 +376,77 @@ int ngtcp2_strm_is_tx_queued(ngtcp2_strm *strm) {
 }
 
 int ngtcp2_strm_is_all_tx_data_acked(ngtcp2_strm *strm) {
-  return ngtcp2_gaptr_first_gap_offset(&strm->tx.acked_offset) ==
+  if (strm->tx.acked_offset == NULL) {
+    return strm->tx.cont_acked_offset == strm->tx.offset;
+  }
+
+  return ngtcp2_gaptr_first_gap_offset(strm->tx.acked_offset) ==
          strm->tx.offset;
 }
+
+ngtcp2_range ngtcp2_strm_get_unacked_range_after(ngtcp2_strm *strm,
+                                                 uint64_t offset) {
+  ngtcp2_ksl_it gapit;
+  ngtcp2_range gap;
+
+  if (strm->tx.acked_offset == NULL) {
+    gap.begin = strm->tx.cont_acked_offset;
+    gap.end = UINT64_MAX;
+    return gap;
+  }
+
+  gapit = ngtcp2_gaptr_get_first_gap_after(strm->tx.acked_offset, offset);
+  return *(ngtcp2_range *)ngtcp2_ksl_it_key(&gapit);
+}
+
+uint64_t ngtcp2_strm_get_acked_offset(ngtcp2_strm *strm) {
+  if (strm->tx.acked_offset == NULL) {
+    return strm->tx.cont_acked_offset;
+  }
+
+  return ngtcp2_gaptr_first_gap_offset(strm->tx.acked_offset);
+}
+
+static int strm_acked_offset_init(ngtcp2_strm *strm) {
+  int rv;
+  ngtcp2_gaptr *acked_offset =
+      ngtcp2_mem_malloc(strm->mem, sizeof(*acked_offset));
+
+  if (acked_offset == NULL) {
+    return NGTCP2_ERR_NOMEM;
+  }
+
+  rv = ngtcp2_gaptr_init(acked_offset, strm->mem);
+  if (rv != 0) {
+    ngtcp2_mem_free(strm->mem, acked_offset);
+    return rv;
+  }
+
+  strm->tx.acked_offset = acked_offset;
+
+  return 0;
+}
+
+int ngtcp2_strm_ack_data(ngtcp2_strm *strm, uint64_t offset, uint64_t len) {
+  int rv;
+
+  if (strm->tx.acked_offset == NULL) {
+    if (strm->tx.cont_acked_offset == offset) {
+      strm->tx.cont_acked_offset += len;
+      return 0;
+    }
+
+    rv = strm_acked_offset_init(strm);
+    if (rv != 0) {
+      return rv;
+    }
+
+    rv =
+        ngtcp2_gaptr_push(strm->tx.acked_offset, 0, strm->tx.cont_acked_offset);
+    if (rv != 0) {
+      return rv;
+    }
+  }
+
+  return ngtcp2_gaptr_push(strm->tx.acked_offset, offset, len);
+}
diff --git a/lib/ngtcp2_strm.h b/lib/ngtcp2_strm.h
index 4d16d4df4b20b3d4e85ea11471b8c08c75d2c845..999c37a63f3318fc6a3aca6633e25e51174c0f1a 100644
--- a/lib/ngtcp2_strm.h
+++ b/lib/ngtcp2_strm.h
@@ -76,7 +76,12 @@ struct ngtcp2_strm {
 
   struct {
     /* acked_offset tracks acknowledged outgoing data. */
-    ngtcp2_gaptr acked_offset;
+    ngtcp2_gaptr *acked_offset;
+    /* cont_acked_offset is the offset that all data up to this offset
+       is acknowledged by a remote endpoint.  It is used until the
+       remote endpoint acknowledges data in out-of-order.  After that,
+       acked_offset is used instead. */
+    uint64_t cont_acked_offset;
     /* streamfrq contains STREAM frame for retransmission.  The flow
        control credits have been paid when they are transmitted first
        time.  There are no restriction regarding flow control for
@@ -229,4 +234,24 @@ int ngtcp2_strm_is_tx_queued(ngtcp2_strm *strm);
  */
 int ngtcp2_strm_is_all_tx_data_acked(ngtcp2_strm *strm);
 
+/*
+ * ngtcp2_strm_get_unacked_range_after returns the range that is not
+ * acknowledged yet and intersects or comes after |offset|.
+ */
+ngtcp2_range ngtcp2_strm_get_unacked_range_after(ngtcp2_strm *strm,
+                                                 uint64_t offset);
+
+/*
+ * ngtcp2_strm_get_acked_offset returns offset, that is the data up to
+ * this offset have been acknowledged by a remote endpoint.  It
+ * returns 0 if no data is acknowledged.
+ */
+uint64_t ngtcp2_strm_get_acked_offset(ngtcp2_strm *strm);
+
+/*
+ * ngtcp2_strm_ack_data tells |strm| that the data [offset,
+ * offset+len) is acknowledged by a remote endpoint.
+ */
+int ngtcp2_strm_ack_data(ngtcp2_strm *strm, uint64_t offset, uint64_t len);
+
 #endif /* NGTCP2_STRM_H */