diff --git a/lib/ngtcp2_conn.c b/lib/ngtcp2_conn.c
index 90b64087ed01cc57e3ef8ae2db6877404019bc51..a4e0882b278c3e9ad90fd2a4cd98570db6c58a79 100644
--- a/lib/ngtcp2_conn.c
+++ b/lib/ngtcp2_conn.c
@@ -1335,10 +1335,10 @@ static size_t conn_retry_early_payloadlen(ngtcp2_conn *conn) {
 /*
  * conn_cryptofrq_unacked_offset returns the CRYPTO frame offset by
  * taking into account acknowledged offset.  If there is no data to
- * send, this function returns (size_t)-1.
+ * send, this function returns (uint64_t)-1.
  */
-static size_t conn_cryptofrq_unacked_offset(ngtcp2_conn *conn,
-                                            ngtcp2_pktns *pktns) {
+static uint64_t conn_cryptofrq_unacked_offset(ngtcp2_conn *conn,
+                                              ngtcp2_pktns *pktns) {
   ngtcp2_frame_chain *frc;
   ngtcp2_crypto *fr;
   ngtcp2_range gap;
@@ -1365,7 +1365,7 @@ static size_t conn_cryptofrq_unacked_offset(ngtcp2_conn *conn,
     }
   }
 
-  return (size_t)-1;
+  return (uint64_t)-1;
 }
 
 static int conn_cryptofrq_unacked_pop(ngtcp2_conn *conn, ngtcp2_pktns *pktns,
@@ -2550,6 +2550,7 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, uint8_t *dest,
   int credit_expanded = 0;
   ngtcp2_cc_pkt cc_pkt;
   uint64_t crypto_offset;
+  uint64_t stream_offset;
 
   /* Return 0 if destlen is less than minimum packet length which can
      trigger Stateless Reset */
@@ -2683,6 +2684,14 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, uint8_t *dest,
     }
 
     for (pfrc = &pktns->tx.frq; *pfrc;) {
+      if ((*pfrc)->binder &&
+          ((*pfrc)->binder->flags & NGTCP2_FRAME_CHAIN_BINDER_FLAG_ACK)) {
+        frc = *pfrc;
+        *pfrc = (*pfrc)->next;
+        ngtcp2_frame_chain_del(frc, conn->mem);
+        continue;
+      }
+
       switch ((*pfrc)->fr.type) {
       case NGTCP2_FRAME_STOP_SENDING:
         strm =
@@ -2887,11 +2896,17 @@ static ngtcp2_ssize conn_write_pkt(ngtcp2_conn *conn, uint8_t *dest,
           continue;
         }
 
+        stream_offset = ngtcp2_strm_streamfrq_unacked_offset(strm);
+        if (stream_offset == (uint64_t)-1) {
+          ngtcp2_strm_streamfrq_clear(strm);
+          ngtcp2_conn_tx_strmq_pop(conn);
+          continue;
+        }
+
         left = ngtcp2_ppe_left(ppe);
 
-        left = ngtcp2_pkt_stream_max_datalen(
-            strm->stream_id, ngtcp2_strm_streamfrq_top(strm)->fr.stream.offset,
-            left, left);
+        left = ngtcp2_pkt_stream_max_datalen(strm->stream_id, stream_offset,
+                                             left, left);
 
         if (left == (size_t)-1) {
           break;
@@ -3807,7 +3822,11 @@ int ngtcp2_conn_detect_lost_pkt(ngtcp2_conn *conn, ngtcp2_pktns *pktns,
   int rv;
   ngtcp2_duration pto = conn_compute_pto(conn, pktns);
 
-  ngtcp2_rtb_detect_lost_pkt(&pktns->rtb, &frc, cstat, pto, ts);
+  rv = ngtcp2_rtb_detect_lost_pkt(&pktns->rtb, &frc, cstat, conn, pto, ts);
+  if (rv != 0) {
+    ngtcp2_frame_chain_list_del(frc, conn->mem);
+    return rv;
+  }
 
   rv = conn_resched_frames(conn, pktns, &frc);
   if (rv != 0) {
diff --git a/lib/ngtcp2_rtb.c b/lib/ngtcp2_rtb.c
index 4b60b07563bc5847044960ba6feedd58c1d5d115..f55e18d62debd75f338ee2b1cd17a0c3c9f8291f 100644
--- a/lib/ngtcp2_rtb.c
+++ b/lib/ngtcp2_rtb.c
@@ -84,11 +84,53 @@ int ngtcp2_frame_chain_crypto_datacnt_new(ngtcp2_frame_chain **pfrc,
   return ngtcp2_frame_chain_new(pfrc, mem);
 }
 
+int ngtcp2_frame_chain_new_token_new(ngtcp2_frame_chain **pfrc,
+                                     const ngtcp2_vec *token,
+                                     const ngtcp2_mem *mem) {
+  size_t avail = sizeof(ngtcp2_frame) - sizeof(ngtcp2_new_token);
+  int rv;
+  uint8_t *p;
+  ngtcp2_frame *fr;
+
+  if (token->len > avail) {
+    rv = ngtcp2_frame_chain_extralen_new(pfrc, token->len - avail, mem);
+  } else {
+    rv = ngtcp2_frame_chain_new(pfrc, mem);
+  }
+  if (rv != 0) {
+    return rv;
+  }
+
+  fr = &(*pfrc)->fr;
+  fr->type = NGTCP2_FRAME_NEW_TOKEN;
+
+  p = (uint8_t *)(*pfrc) + sizeof(ngtcp2_new_token);
+  memcpy(p, token->base, token->len);
+
+  ngtcp2_vec_init(&fr->new_token.token, p, token->len);
+
+  return 0;
+}
+
 void ngtcp2_frame_chain_del(ngtcp2_frame_chain *frc, const ngtcp2_mem *mem) {
+  ngtcp2_frame_chain_binder *binder;
+
+  if (frc == NULL) {
+    return;
+  }
+
+  binder = frc->binder;
+  if (binder && --binder->refcount == 0) {
+    ngtcp2_mem_free(mem, binder);
+  }
+
   ngtcp2_mem_free(mem, frc);
 }
 
-void ngtcp2_frame_chain_init(ngtcp2_frame_chain *frc) { frc->next = NULL; }
+void ngtcp2_frame_chain_init(ngtcp2_frame_chain *frc) {
+  frc->next = NULL;
+  frc->binder = NULL;
+}
 
 void ngtcp2_frame_chain_list_del(ngtcp2_frame_chain *frc,
                                  const ngtcp2_mem *mem) {
@@ -96,7 +138,7 @@ void ngtcp2_frame_chain_list_del(ngtcp2_frame_chain *frc,
 
   for (; frc;) {
     next = frc->next;
-    ngtcp2_mem_free(mem, frc);
+    ngtcp2_frame_chain_del(frc, mem);
     frc = next;
   }
 }
@@ -114,6 +156,39 @@ static void frame_chain_insert(ngtcp2_frame_chain **pfrc,
   *pfrc = frc;
 }
 
+int ngtcp2_frame_chain_binder_new(ngtcp2_frame_chain_binder **pbinder,
+                                  const ngtcp2_mem *mem) {
+  *pbinder = ngtcp2_mem_calloc(mem, 1, sizeof(ngtcp2_frame_chain_binder));
+  if (*pbinder == NULL) {
+    return NGTCP2_ERR_NOMEM;
+  }
+
+  return 0;
+}
+
+int ngtcp2_bind_frame_chains(ngtcp2_frame_chain *a, ngtcp2_frame_chain *b,
+                             const ngtcp2_mem *mem) {
+  ngtcp2_frame_chain_binder *binder;
+  int rv;
+
+  assert(b->binder == NULL);
+
+  if (a->binder == NULL) {
+    rv = ngtcp2_frame_chain_binder_new(&binder, mem);
+    if (rv != 0) {
+      return rv;
+    }
+
+    a->binder = binder;
+    ++a->binder->refcount;
+  }
+
+  b->binder = a->binder;
+  ++b->binder->refcount;
+
+  return 0;
+}
+
 int ngtcp2_rtb_entry_new(ngtcp2_rtb_entry **pent, const ngtcp2_pkt_hd *hd,
                          ngtcp2_frame_chain *frc, ngtcp2_tstamp ts,
                          size_t pktlen, uint8_t flags, const ngtcp2_mem *mem) {
@@ -127,6 +202,7 @@ int ngtcp2_rtb_entry_new(ngtcp2_rtb_entry **pent, const ngtcp2_pkt_hd *hd,
   (*pent)->hd.flags = hd->flags;
   (*pent)->frc = frc;
   (*pent)->ts = ts;
+  (*pent)->lost_ts = UINT64_MAX;
   (*pent)->pktlen = pktlen;
   (*pent)->flags = flags;
 
@@ -201,6 +277,10 @@ static void rtb_on_add(ngtcp2_rtb *rtb, ngtcp2_rtb_entry *ent,
 
 static void rtb_on_remove(ngtcp2_rtb *rtb, ngtcp2_rtb_entry *ent,
                           ngtcp2_conn_stat *cstat) {
+  if (ent->flags & NGTCP2_RTB_FLAG_LOST_RETRANSMITTED) {
+    return;
+  }
+
   if (ent->flags & NGTCP2_RTB_FLAG_ACK_ELICITING) {
     assert(rtb->num_ack_eliciting);
     --rtb->num_ack_eliciting;
@@ -215,8 +295,15 @@ static void rtb_on_remove(ngtcp2_rtb *rtb, ngtcp2_rtb_entry *ent,
   }
 }
 
-static void rtb_on_pkt_lost(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
-                            ngtcp2_rtb_entry *ent) {
+static int rtb_on_pkt_lost(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
+                           ngtcp2_ksl_it *it, ngtcp2_rtb_entry *ent,
+                           ngtcp2_conn *conn, ngtcp2_tstamp ts) {
+  ngtcp2_frame_chain *first = *pfrc, *frc, *nfrc;
+  ngtcp2_frame *fr;
+  ngtcp2_range gap, range;
+  ngtcp2_strm *strm;
+  int rv;
+
   ngtcp2_log_pkt_lost(rtb->log, ent->hd.pkt_num, ent->hd.type, ent->hd.flags,
                       ent->ts);
 
@@ -229,12 +316,125 @@ static void rtb_on_pkt_lost(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
       ngtcp2_log_info(rtb->log, NGTCP2_LOG_EVENT_RCV,
                       "pkn=%" PRId64 " CRYPTO has already been retransmitted",
                       ent->hd.pkt_num);
-    } else if (ent->frc) {
+      assert(!(ent->flags & NGTCP2_RTB_FLAG_LOST_RETRANSMITTED));
+      assert(UINT64_MAX == ent->lost_ts);
+
+      ent->flags |= NGTCP2_RTB_FLAG_LOST_RETRANSMITTED;
+      ent->lost_ts = ts;
+
+      ngtcp2_ksl_it_next(it);
+
+      return 0;
+    }
+
+    if (ent->frc) {
+      assert(!(ent->flags & NGTCP2_RTB_FLAG_LOST_RETRANSMITTED));
+      assert(UINT64_MAX == ent->lost_ts);
+
       /* PADDING only (or PADDING + ACK ) packets will have NULL
          ent->frc. */
       /* TODO Reconsider the order of pfrc */
-      frame_chain_insert(pfrc, ent->frc);
-      ent->frc = NULL;
+      for (frc = ent->frc; frc; frc = frc->next) {
+        fr = &frc->fr;
+        /* Check that a late ACK acknowledged this frame. */
+        if (frc->binder &&
+            (frc->binder->flags & NGTCP2_FRAME_CHAIN_BINDER_FLAG_ACK)) {
+          continue;
+        }
+        switch (frc->fr.type) {
+        case NGTCP2_FRAME_STREAM:
+          strm = ngtcp2_conn_find_stream(conn, fr->stream.stream_id);
+          if (strm == NULL) {
+            continue;
+          }
+
+          gap = ngtcp2_strm_get_unacked_range_after(strm, fr->stream.offset);
+
+          range.begin = fr->stream.offset;
+          range.end = fr->stream.offset +
+                      ngtcp2_vec_len(fr->stream.data, fr->stream.datacnt);
+          range = ngtcp2_range_intersect(&range, &gap);
+          if (ngtcp2_range_len(&range) == 0 &&
+              (!fr->stream.fin || (strm->flags & NGTCP2_STRM_FLAG_FIN_ACKED))) {
+            continue;
+          }
+
+          rv = ngtcp2_frame_chain_stream_datacnt_new(&nfrc, fr->stream.datacnt,
+                                                     rtb->mem);
+          if (rv != 0) {
+            return rv;
+          }
+
+          nfrc->fr = *fr;
+          ngtcp2_vec_copy(nfrc->fr.stream.data, fr->stream.data,
+                          fr->stream.datacnt);
+
+          break;
+        case NGTCP2_FRAME_CRYPTO:
+          /* Don't resend CRYPTO frame if the whole region it contains has
+             been acknowledged */
+          gap = ngtcp2_strm_get_unacked_range_after(rtb->crypto,
+                                                    fr->crypto.offset);
+
+          range.begin = fr->crypto.offset;
+          range.end = fr->crypto.offset +
+                      ngtcp2_vec_len(fr->crypto.data, fr->crypto.datacnt);
+          range = ngtcp2_range_intersect(&range, &gap);
+          if (ngtcp2_range_len(&range) == 0) {
+            continue;
+          }
+
+          rv = ngtcp2_frame_chain_crypto_datacnt_new(&nfrc, fr->crypto.datacnt,
+                                                     rtb->mem);
+          if (rv != 0) {
+            return rv;
+          }
+
+          nfrc->fr = *fr;
+          ngtcp2_vec_copy(nfrc->fr.crypto.data, fr->crypto.data,
+                          fr->crypto.datacnt);
+
+          break;
+        case NGTCP2_FRAME_NEW_TOKEN:
+          rv = ngtcp2_frame_chain_new_token_new(&nfrc, &fr->new_token.token,
+                                                rtb->mem);
+          if (rv != 0) {
+            return rv;
+          }
+
+          rv = ngtcp2_bind_frame_chains(frc, nfrc, rtb->mem);
+          if (rv != 0) {
+            return rv;
+          }
+
+          break;
+        default:
+          rv = ngtcp2_frame_chain_new(&nfrc, rtb->mem);
+          if (rv != 0) {
+            return rv;
+          }
+
+          nfrc->fr = *fr;
+
+          rv = ngtcp2_bind_frame_chains(frc, nfrc, rtb->mem);
+          if (rv != 0) {
+            return rv;
+          }
+
+          break;
+        }
+
+        frame_chain_insert(pfrc, nfrc);
+      }
+
+      if (*pfrc != first) {
+        ent->flags |= NGTCP2_RTB_FLAG_LOST_RETRANSMITTED;
+        ent->lost_ts = ts;
+
+        ngtcp2_ksl_it_next(it);
+
+        return 0;
+      }
     }
   } else {
     ngtcp2_log_info(rtb->log, NGTCP2_LOG_EVENT_RCV,
@@ -243,7 +443,10 @@ static void rtb_on_pkt_lost(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
                     ent->hd.pkt_num);
   }
 
+  ngtcp2_ksl_remove(&rtb->ents, it, &ent->hd.pkt_num);
   ngtcp2_rtb_entry_del(ent, rtb->mem);
+
+  return 0;
 }
 
 int ngtcp2_rtb_add(ngtcp2_rtb *rtb, ngtcp2_rtb_entry *ent,
@@ -284,12 +487,21 @@ static int rtb_process_acked_pkt(ngtcp2_rtb *rtb, ngtcp2_rtb_entry *ent,
   ngtcp2_crypto_level crypto_level;
 
   for (frc = ent->frc; frc; frc = frc->next) {
+    if (frc->binder) {
+      frc->binder->flags |= NGTCP2_FRAME_CHAIN_BINDER_FLAG_ACK;
+    }
+
     switch (frc->fr.type) {
     case NGTCP2_FRAME_STREAM:
       strm = ngtcp2_conn_find_stream(conn, frc->fr.stream.stream_id);
       if (strm == NULL) {
         break;
       }
+
+      if (frc->fr.stream.fin) {
+        strm->flags |= NGTCP2_STRM_FLAG_FIN_ACKED;
+      }
+
       prev_stream_offset = ngtcp2_strm_get_acked_offset(strm);
       rv = ngtcp2_strm_ack_data(
           strm, frc->fr.stream.offset,
@@ -544,9 +756,9 @@ static ngtcp2_duration compute_pkt_loss_delay(const ngtcp2_conn_stat *cstat) {
   return ngtcp2_max(loss_delay, NGTCP2_GRANULARITY);
 }
 
-void ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
-                                ngtcp2_conn_stat *cstat, ngtcp2_duration pto,
-                                ngtcp2_tstamp ts) {
+int ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
+                               ngtcp2_conn_stat *cstat, ngtcp2_conn *conn,
+                               ngtcp2_duration pto, ngtcp2_tstamp ts) {
   ngtcp2_rtb_entry *ent;
   ngtcp2_duration loss_delay;
   ngtcp2_tstamp lost_send_time;
@@ -555,6 +767,7 @@ void ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
   int64_t last_lost_pkt_num;
   ngtcp2_duration loss_window, congestion_period;
   ngtcp2_cc *cc = rtb->cc;
+  int rv;
 
   cstat->loss_time[rtb->pktns_id] = UINT64_MAX;
   loss_delay = compute_pkt_loss_delay(cstat);
@@ -564,14 +777,19 @@ void ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
   for (; !ngtcp2_ksl_it_end(&it); ngtcp2_ksl_it_next(&it)) {
     ent = ngtcp2_ksl_it_get(&it);
 
+    if (ent->flags & NGTCP2_RTB_FLAG_LOST_RETRANSMITTED) {
+      break;
+    }
+
     if (rtb_pkt_lost(rtb, cstat, ent, loss_delay, lost_send_time)) {
       /* All entries from ent are considered to be lost. */
       latest_ts = oldest_ts = ent->ts;
       last_lost_pkt_num = ent->hd.pkt_num;
 
+      congestion_period = pto * NGTCP2_PERSISTENT_CONGESTION_THRESHOLD;
+
       for (; !ngtcp2_ksl_it_end(&it);) {
         ent = ngtcp2_ksl_it_get(&it);
-        ngtcp2_ksl_remove(&rtb->ents, &it, &ent->hd.pkt_num);
 
         if (last_lost_pkt_num == ent->hd.pkt_num + 1 &&
             ent->ts >= rtb->persistent_congestion_start_ts) {
@@ -581,8 +799,20 @@ void ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
           last_lost_pkt_num = -1;
         }
 
+        if ((ent->flags & NGTCP2_RTB_FLAG_LOST_RETRANSMITTED)) {
+          if (rtb->pktns_id != NGTCP2_PKTNS_ID_APP || last_lost_pkt_num == -1 ||
+              latest_ts - oldest_ts >= congestion_period) {
+            break;
+          }
+          ngtcp2_ksl_it_next(&it);
+          continue;
+        }
+
         rtb_on_remove(rtb, ent, cstat);
-        rtb_on_pkt_lost(rtb, pfrc, ent);
+        rv = rtb_on_pkt_lost(rtb, pfrc, &it, ent, conn, ts);
+        if (rv != 0) {
+          return rv;
+        }
       }
 
       cc->congestion_event(cc, cstat, latest_ts, ts);
@@ -596,7 +826,6 @@ void ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
        * not enable it during handshake.
        */
       if (rtb->pktns_id == NGTCP2_PKTNS_ID_APP && loss_window > 0) {
-        congestion_period = pto * NGTCP2_PERSISTENT_CONGESTION_THRESHOLD;
         if (loss_window >= congestion_period) {
           ngtcp2_log_info(rtb->log, NGTCP2_LOG_EVENT_RCV,
                           "persistent congestion loss_window=%" PRIu64
@@ -607,11 +836,73 @@ void ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
         }
       }
 
-      return;
+      break;
+    }
+  }
+
+  if (ngtcp2_ksl_len(&rtb->ents) == 0) {
+    return 0;
+  }
+
+  it = ngtcp2_ksl_end(&rtb->ents);
+
+  for (;;) {
+    ngtcp2_ksl_it_prev(&it);
+    ent = ngtcp2_ksl_it_get(&it);
+
+    if (!(ent->flags & NGTCP2_RTB_FLAG_LOST_RETRANSMITTED) ||
+        ts - ent->lost_ts < pto) {
+      return 0;
+    }
+
+    ngtcp2_log_info(rtb->log, NGTCP2_LOG_EVENT_RCV,
+                    "removing stale lost packet pkn=%" PRId64, ent->hd.pkt_num);
+
+    ngtcp2_ksl_remove(&rtb->ents, &it, &ent->hd.pkt_num);
+    ngtcp2_rtb_entry_del(ent, rtb->mem);
+
+    if (ngtcp2_ksl_len(&rtb->ents) == 0) {
+      return 0;
     }
   }
 }
 
+static void rtb_on_pkt_lost2(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
+                             ngtcp2_rtb_entry *ent) {
+  ngtcp2_log_pkt_lost(rtb->log, ent->hd.pkt_num, ent->hd.type, ent->hd.flags,
+                      ent->ts);
+
+  if (rtb->qlog) {
+    ngtcp2_qlog_pkt_lost(rtb->qlog, ent);
+  }
+
+  if (!(ent->flags & NGTCP2_RTB_FLAG_PROBE)) {
+    if (ent->flags & NGTCP2_RTB_FLAG_CRYPTO_TIMEOUT_RETRANSMITTED) {
+      ngtcp2_log_info(rtb->log, NGTCP2_LOG_EVENT_RCV,
+                      "pkn=%" PRId64 " CRYPTO has already been retransmitted",
+                      ent->hd.pkt_num);
+    } else if (ent->flags & NGTCP2_RTB_FLAG_LOST_RETRANSMITTED) {
+      ngtcp2_log_info(rtb->log, NGTCP2_LOG_EVENT_RCV,
+                      "pkn=%" PRId64
+                      " was declared lost and has already been retransmitted",
+                      ent->hd.pkt_num);
+    } else if (ent->frc) {
+      /* PADDING only (or PADDING + ACK ) packets will have NULL
+         ent->frc. */
+      /* TODO Reconsider the order of pfrc */
+      frame_chain_insert(pfrc, ent->frc);
+      ent->frc = NULL;
+    }
+  } else {
+    ngtcp2_log_info(rtb->log, NGTCP2_LOG_EVENT_RCV,
+                    "pkn=%" PRId64
+                    " is a probe packet, no retransmission is necessary",
+                    ent->hd.pkt_num);
+  }
+
+  ngtcp2_rtb_entry_del(ent, rtb->mem);
+}
+
 void ngtcp2_rtb_remove_all(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
                            ngtcp2_conn_stat *cstat) {
   ngtcp2_rtb_entry *ent;
@@ -625,7 +916,7 @@ void ngtcp2_rtb_remove_all(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
     rtb_on_remove(rtb, ent, cstat);
     ngtcp2_ksl_remove(&rtb->ents, &it, &ent->hd.pkt_num);
 
-    rtb_on_pkt_lost(rtb, pfrc, ent);
+    rtb_on_pkt_lost2(rtb, pfrc, ent);
   }
 }
 
diff --git a/lib/ngtcp2_rtb.h b/lib/ngtcp2_rtb.h
index 4b523e6ef00faeef2d5856d375520c0347785655..c1a12d0278d2e17bc376b60340a9ed4775ba7898 100644
--- a/lib/ngtcp2_rtb.h
+++ b/lib/ngtcp2_rtb.h
@@ -53,11 +53,30 @@ typedef struct ngtcp2_strm ngtcp2_strm;
 struct ngtcp2_rst;
 typedef struct ngtcp2_rst ngtcp2_rst;
 
+typedef enum ngtcp2_frame_chain_binder_flag {
+  NGTCP2_FRAME_CHAIN_BINDER_FLAG_NONE = 0x00,
+  /* NGTCP2_FRAME_CHAIN_BINDER_FLAG_ACK indicates that an information
+     which a frame carries has been acknowledged. */
+  NGTCP2_FRAME_CHAIN_BINDER_FLAG_ACK = 0x01,
+} ngtcp2_frame_chain_binder_flag;
+
+typedef struct ngtcp2_frame_chain_binder {
+  size_t refcount;
+  uint32_t flags;
+} ngtcp2_frame_chain_binder;
+
+int ngtcp2_frame_chain_binder_new(ngtcp2_frame_chain_binder **pbinder,
+                                  const ngtcp2_mem *mem);
+
+int ngtcp2_bind_frame_chains(ngtcp2_frame_chain *a, ngtcp2_frame_chain *b,
+                             const ngtcp2_mem *mem);
+
 /*
  * ngtcp2_frame_chain chains frames in a single packet.
  */
 struct ngtcp2_frame_chain {
   ngtcp2_frame_chain *next;
+  ngtcp2_frame_chain_binder *binder;
   ngtcp2_frame fr;
 };
 
@@ -111,6 +130,10 @@ int ngtcp2_frame_chain_crypto_datacnt_new(ngtcp2_frame_chain **pfrc,
                                           size_t datacnt,
                                           const ngtcp2_mem *mem);
 
+int ngtcp2_frame_chain_new_token_new(ngtcp2_frame_chain **pfrc,
+                                     const ngtcp2_vec *token,
+                                     const ngtcp2_mem *mem);
+
 /*
  * ngtcp2_frame_chain_del deallocates |frc|.  It also deallocates the
  * memory pointed by |frc|.
@@ -143,6 +166,9 @@ typedef enum {
   /* NGTCP2_RTB_FLAG_CRYPTO_TIMEOUT_RETRANSMITTED indicates that the
      CRYPTO frames have been retransmitted. */
   NGTCP2_RTB_FLAG_CRYPTO_TIMEOUT_RETRANSMITTED = 0x08,
+  /* NGTCP2_RTB_FLAG_LOST_RETRANSMITTED indicates that the entry has
+     been marked lost and scheduled to retransmit. */
+  NGTCP2_RTB_FLAG_LOST_RETRANSMITTED = 0x10,
 } ngtcp2_rtb_flag;
 
 struct ngtcp2_rtb_entry;
@@ -164,6 +190,7 @@ struct ngtcp2_rtb_entry {
   /* ts is the time point when a packet included in this entry is sent
      to a peer. */
   ngtcp2_tstamp ts;
+  ngtcp2_tstamp lost_ts;
   /* pktlen is the length of QUIC packet */
   size_t pktlen;
   struct {
@@ -290,9 +317,9 @@ ngtcp2_ssize ngtcp2_rtb_recv_ack(ngtcp2_rtb *rtb, const ngtcp2_ack *fr,
  * some frames might be prepended to |*pfrc| and the caller should
  * handle them.  |pto| is PTO.
  */
-void ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
-                                ngtcp2_conn_stat *cstat, ngtcp2_duration pto,
-                                ngtcp2_tstamp ts);
+int ngtcp2_rtb_detect_lost_pkt(ngtcp2_rtb *rtb, ngtcp2_frame_chain **pfrc,
+                               ngtcp2_conn_stat *cstat, ngtcp2_conn *conn,
+                               ngtcp2_duration pto, ngtcp2_tstamp ts);
 
 /*
  * ngtcp2_rtb_remove_all removes all packets from |rtb| and prepends
diff --git a/lib/ngtcp2_strm.c b/lib/ngtcp2_strm.c
index 214683de9d31e30e1db9e5a66dd5f459b18d4bb9..2a0c0c918982323dd8439a74f512e58d56d8c7e3 100644
--- a/lib/ngtcp2_strm.c
+++ b/lib/ngtcp2_strm.c
@@ -178,6 +178,163 @@ int ngtcp2_strm_streamfrq_push(ngtcp2_strm *strm, ngtcp2_frame_chain *frc) {
                            frc);
 }
 
+static int strm_streamfrq_unacked_pop(ngtcp2_strm *strm,
+                                      ngtcp2_frame_chain **pfrc) {
+  ngtcp2_frame_chain *frc, *nfrc;
+  ngtcp2_stream *fr, *nfr;
+  uint64_t offset, end_offset;
+  size_t idx, end_idx;
+  uint64_t base_offset, end_base_offset;
+  ngtcp2_range gap;
+  ngtcp2_vec *v;
+  int rv;
+  ngtcp2_ksl_it it;
+
+  *pfrc = NULL;
+
+  assert(strm->tx.streamfrq);
+  assert(ngtcp2_ksl_len(strm->tx.streamfrq));
+
+  for (it = ngtcp2_ksl_begin(strm->tx.streamfrq); !ngtcp2_ksl_it_end(&it);) {
+    frc = ngtcp2_ksl_it_get(&it);
+    fr = &frc->fr.stream;
+
+    ngtcp2_ksl_remove(strm->tx.streamfrq, &it, &fr->offset);
+
+    idx = 0;
+    offset = fr->offset;
+    base_offset = 0;
+
+    gap = ngtcp2_strm_get_unacked_range_after(strm, offset);
+    if (gap.begin < offset) {
+      gap.begin = offset;
+    }
+
+    for (; idx < fr->datacnt && offset < gap.begin; ++idx) {
+      v = &fr->data[idx];
+      if (offset + v->len > gap.begin) {
+        base_offset = gap.begin - offset;
+        break;
+      }
+
+      offset += v->len;
+    }
+
+    if (idx == fr->datacnt) {
+      if (fr->fin) {
+        if (strm->flags & NGTCP2_STRM_FLAG_FIN_ACKED) {
+          ngtcp2_frame_chain_del(frc, strm->mem);
+          assert(ngtcp2_ksl_len(strm->tx.streamfrq) == 0);
+          return 0;
+        }
+
+        rv = ngtcp2_frame_chain_new(&nfrc, strm->mem);
+        if (rv != 0) {
+          ngtcp2_frame_chain_del(frc, strm->mem);
+          return rv;
+        }
+
+        nfr = &nfrc->fr.stream;
+        nfr->fin = 1;
+        nfr->stream_id = fr->stream_id;
+        nfr->offset = fr->offset + ngtcp2_vec_len(fr->data, fr->datacnt);
+        nfr->datacnt = 0;
+
+        *pfrc = nfrc;
+      }
+      ngtcp2_frame_chain_del(frc, strm->mem);
+      continue;
+    }
+
+    assert(gap.begin == offset + base_offset);
+
+    end_idx = idx;
+    end_offset = offset;
+    end_base_offset = 0;
+
+    for (; end_idx < fr->datacnt; ++end_idx) {
+      v = &fr->data[end_idx];
+      if (end_offset + v->len > gap.end) {
+        end_base_offset = gap.end - end_offset;
+        break;
+      }
+
+      end_offset += v->len;
+    }
+
+    if (fr->offset == offset && base_offset == 0 && fr->datacnt == end_idx) {
+      *pfrc = frc;
+      return 0;
+    }
+
+    if (fr->datacnt == end_idx) {
+      memmove(fr->data, fr->data + idx, sizeof(fr->data[0]) * (end_idx - idx));
+
+      assert(fr->data[0].len > base_offset);
+
+      fr->offset = offset + base_offset;
+      fr->datacnt = end_idx - idx;
+      fr->data[0].base += base_offset;
+      fr->data[0].len -= (size_t)base_offset;
+
+      *pfrc = frc;
+      return 0;
+    }
+
+    rv = ngtcp2_frame_chain_stream_datacnt_new(&nfrc, fr->datacnt - end_idx,
+                                               strm->mem);
+    if (rv != 0) {
+      ngtcp2_frame_chain_del(frc, strm->mem);
+      return rv;
+    }
+
+    nfr = &nfrc->fr.stream;
+    memcpy(nfr->data, fr->data + end_idx,
+           sizeof(nfr->data[0]) * (fr->datacnt - end_idx));
+
+    assert(nfr->data[0].len > end_base_offset);
+
+    nfr->flags = 0;
+    nfr->fin = fr->fin;
+    nfr->stream_id = fr->stream_id;
+    nfr->offset = end_offset + end_base_offset;
+    nfr->datacnt = fr->datacnt - end_idx;
+    nfr->data[0].base += end_base_offset;
+    nfr->data[0].len -= (size_t)end_base_offset;
+
+    rv = ngtcp2_ksl_insert(strm->tx.streamfrq, NULL, &nfr->offset, nfrc);
+    if (rv != 0) {
+      assert(ngtcp2_err_is_fatal(rv));
+      ngtcp2_frame_chain_del(nfrc, strm->mem);
+      ngtcp2_frame_chain_del(frc, strm->mem);
+      return rv;
+    }
+
+    if (end_base_offset) {
+      ++end_idx;
+    }
+
+    memmove(fr->data, fr->data + idx, sizeof(fr->data[0]) * (end_idx - idx));
+
+    assert(fr->data[0].len > base_offset);
+
+    fr->fin = 0;
+    fr->offset = offset + base_offset;
+    fr->datacnt = end_idx - idx;
+    if (end_base_offset) {
+      assert(fr->data[fr->datacnt - 1].len > end_base_offset);
+      fr->data[fr->datacnt - 1].len = (size_t)end_base_offset;
+    }
+    fr->data[0].base += base_offset;
+    fr->data[0].len -= (size_t)base_offset;
+
+    *pfrc = frc;
+    return 0;
+  }
+
+  return 0;
+}
+
 int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc,
                               size_t left) {
   ngtcp2_stream *fr, *nfr;
@@ -188,18 +345,23 @@ int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc,
   ngtcp2_vec a[NGTCP2_MAX_STREAM_DATACNT];
   ngtcp2_vec b[NGTCP2_MAX_STREAM_DATACNT];
   size_t acnt, bcnt;
-  ngtcp2_ksl_it it;
-  uint64_t old_offset;
+  uint64_t unacked_offset;
 
   if (strm->tx.streamfrq == NULL || ngtcp2_ksl_len(strm->tx.streamfrq) == 0) {
     *pfrc = NULL;
     return 0;
   }
 
-  it = ngtcp2_ksl_begin(strm->tx.streamfrq);
-  frc = ngtcp2_ksl_it_get(&it);
-  fr = &frc->fr.stream;
+  rv = strm_streamfrq_unacked_pop(strm, &frc);
+  if (rv != 0) {
+    return rv;
+  }
+  if (frc == NULL) {
+    *pfrc = NULL;
+    return 0;
+  }
 
+  fr = &frc->fr.stream;
   datalen = ngtcp2_vec_len(fr->data, fr->datacnt);
 
   if (left == 0) {
@@ -210,8 +372,6 @@ int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc,
     }
   }
 
-  ngtcp2_ksl_remove(strm->tx.streamfrq, NULL, &fr->offset);
-
   if (datalen > left) {
     ngtcp2_vec_copy(a, fr->data, fr->datacnt);
     acnt = fr->datacnt;
@@ -272,18 +432,26 @@ int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc,
   acnt = fr->datacnt;
 
   for (; left && ngtcp2_ksl_len(strm->tx.streamfrq);) {
-    it = ngtcp2_ksl_begin(strm->tx.streamfrq);
-    nfrc = ngtcp2_ksl_it_get(&it);
-    nfr = &nfrc->fr.stream;
+    unacked_offset = ngtcp2_strm_streamfrq_unacked_offset(strm);
+    if (unacked_offset != fr->offset + datalen) {
+      assert(fr->offset + datalen < unacked_offset);
+      break;
+    }
 
-    if (nfr->offset != fr->offset + datalen) {
-      assert(fr->offset + datalen < nfr->offset);
+    rv = strm_streamfrq_unacked_pop(strm, &nfrc);
+    if (rv != 0) {
+      assert(ngtcp2_err_is_fatal(rv));
+      ngtcp2_frame_chain_del(frc, strm->mem);
+      return rv;
+    }
+    if (nfrc == NULL) {
       break;
     }
 
+    nfr = &nfrc->fr.stream;
+
     if (nfr->fin && nfr->datacnt == 0) {
       fr->fin = 1;
-      ngtcp2_ksl_remove(strm->tx.streamfrq, NULL, &nfr->offset);
       ngtcp2_frame_chain_del(nfrc, strm->mem);
       break;
     }
@@ -291,6 +459,13 @@ int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc,
     nmerged = ngtcp2_vec_merge(a, &acnt, nfr->data, &nfr->datacnt, left,
                                NGTCP2_MAX_STREAM_DATACNT);
     if (nmerged == 0) {
+      rv = ngtcp2_ksl_insert(strm->tx.streamfrq, NULL, &nfr->offset, nfrc);
+      if (rv != 0) {
+        assert(ngtcp2_err_is_fatal(rv));
+        ngtcp2_frame_chain_del(nfrc, strm->mem);
+        ngtcp2_frame_chain_del(frc, strm->mem);
+        return rv;
+      }
       break;
     }
 
@@ -299,15 +474,18 @@ int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc,
 
     if (nfr->datacnt == 0) {
       fr->fin = nfr->fin;
-      ngtcp2_ksl_remove(strm->tx.streamfrq, NULL, &nfr->offset);
       ngtcp2_frame_chain_del(nfrc, strm->mem);
       continue;
     }
 
-    old_offset = nfr->offset;
     nfr->offset += nmerged;
 
-    ngtcp2_ksl_update_key(strm->tx.streamfrq, &old_offset, &nfr->offset);
+    rv = ngtcp2_ksl_insert(strm->tx.streamfrq, NULL, &nfr->offset, nfrc);
+    if (rv != 0) {
+      ngtcp2_frame_chain_del(nfrc, strm->mem);
+      ngtcp2_frame_chain_del(frc, strm->mem);
+      return rv;
+    }
 
     break;
   }
@@ -341,6 +519,40 @@ int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc,
   return 0;
 }
 
+uint64_t ngtcp2_strm_streamfrq_unacked_offset(ngtcp2_strm *strm) {
+  ngtcp2_frame_chain *frc;
+  ngtcp2_stream *fr;
+  ngtcp2_range gap;
+  ngtcp2_ksl_it it;
+  size_t datalen;
+
+  assert(strm->tx.streamfrq);
+  assert(ngtcp2_ksl_len(strm->tx.streamfrq));
+
+  for (it = ngtcp2_ksl_begin(strm->tx.streamfrq); !ngtcp2_ksl_it_end(&it);
+       ngtcp2_ksl_it_next(&it)) {
+    frc = ngtcp2_ksl_it_get(&it);
+    fr = &frc->fr.stream;
+
+    gap = ngtcp2_strm_get_unacked_range_after(strm, fr->offset);
+
+    datalen = ngtcp2_vec_len(fr->data, fr->datacnt);
+
+    if (gap.begin <= fr->offset) {
+      return fr->offset;
+    }
+    if (gap.begin < fr->offset + datalen) {
+      return gap.begin;
+    }
+    if (fr->offset + datalen == gap.begin && fr->fin &&
+        !(strm->flags & NGTCP2_STRM_FLAG_FIN_ACKED)) {
+      return fr->offset + datalen;
+    }
+  }
+
+  return (uint64_t)-1;
+}
+
 ngtcp2_frame_chain *ngtcp2_strm_streamfrq_top(ngtcp2_strm *strm) {
   ngtcp2_ksl_it it;
 
diff --git a/lib/ngtcp2_strm.h b/lib/ngtcp2_strm.h
index 999c37a63f3318fc6a3aca6633e25e51174c0f1a..f376ab8878071783440285113020dbd9302919dd 100644
--- a/lib/ngtcp2_strm.h
+++ b/lib/ngtcp2_strm.h
@@ -64,6 +64,7 @@ typedef enum {
   /* NGTCP2_STRM_FLAG_RST_ACKED indicates that the outgoing RST_STREAM
      is acknowledged by peer. */
   NGTCP2_STRM_FLAG_RST_ACKED = 0x20,
+  NGTCP2_STRM_FLAG_FIN_ACKED = 0x40,
 } ngtcp2_strm_flags;
 
 struct ngtcp2_strm;
@@ -207,6 +208,8 @@ int ngtcp2_strm_streamfrq_push(ngtcp2_strm *strm, ngtcp2_frame_chain *frc);
 int ngtcp2_strm_streamfrq_pop(ngtcp2_strm *strm, ngtcp2_frame_chain **pfrc,
                               size_t left);
 
+uint64_t ngtcp2_strm_streamfrq_unacked_offset(ngtcp2_strm *strm);
+
 /*
  * ngtcp2_strm_streamfrq_top returns the first ngtcp2_frame_chain.
  * The queue must not be empty.
diff --git a/tests/main.c b/tests/main.c
index 8b86739485b79ae83cb255fe1e1052f069a34bec..5ffc4c7f92e67e8358fe88dd9966d7e84e8dd57c 100644
--- a/tests/main.c
+++ b/tests/main.c
@@ -269,6 +269,10 @@ int main() {
       !CU_add_test(pSuite, "vec_merge", test_ngtcp2_vec_merge) ||
       !CU_add_test(pSuite, "strm_streamfrq_pop",
                    test_ngtcp2_strm_streamfrq_pop) ||
+      !CU_add_test(pSuite, "strm_streamfrq_unacked_offset",
+                   test_ngtcp2_strm_streamfrq_unacked_offset) ||
+      !CU_add_test(pSuite, "strm_streamfrq_unacked_pop",
+                   test_ngtcp2_strm_streamfrq_unacked_pop) ||
       !CU_add_test(pSuite, "pv_add_entry", test_ngtcp2_pv_add_entry) ||
       !CU_add_test(pSuite, "pv_validate", test_ngtcp2_pv_validate) ||
       !CU_add_test(pSuite, "check_invalid_stateless_reset_token",
diff --git a/tests/ngtcp2_strm_test.c b/tests/ngtcp2_strm_test.c
index e4a7011f0857a18a6c32b5115a0b878b61e20cb7..21f0ecd1886372761271cfc16bf86be2b0c85502 100644
--- a/tests/ngtcp2_strm_test.c
+++ b/tests/ngtcp2_strm_test.c
@@ -28,6 +28,7 @@
 
 #include "ngtcp2_strm.h"
 #include "ngtcp2_test_helper.h"
+#include "ngtcp2_vec.h"
 
 static uint8_t nulldata[1024];
 
@@ -290,3 +291,245 @@ void test_ngtcp2_strm_streamfrq_pop(void) {
   ngtcp2_frame_chain_del(frc, mem);
   ngtcp2_strm_free(&strm);
 }
+
+void test_ngtcp2_strm_streamfrq_unacked_offset(void) {
+  ngtcp2_strm strm;
+  ngtcp2_frame_chain *frc;
+  const ngtcp2_mem *mem = ngtcp2_mem_default();
+  ngtcp2_vec *data;
+
+  /* Everything acknowledged including fin */
+  ngtcp2_strm_init(&strm, 0, NGTCP2_STRM_FLAG_FIN_ACKED, 0, 0, NULL, mem);
+
+  ngtcp2_frame_chain_new(&frc, mem);
+  frc->fr.stream.type = NGTCP2_FRAME_STREAM;
+  frc->fr.stream.fin = 0;
+  frc->fr.stream.offset = 0;
+  frc->fr.stream.datacnt = 1;
+  data = frc->fr.stream.data;
+  data[0].len = 17;
+  data[0].base = nulldata;
+
+  ngtcp2_strm_streamfrq_push(&strm, frc);
+
+  ngtcp2_frame_chain_new(&frc, mem);
+  frc->fr.stream.type = NGTCP2_FRAME_STREAM;
+  frc->fr.stream.fin = 1;
+  frc->fr.stream.offset = 443;
+  frc->fr.stream.datacnt = 1;
+  data = frc->fr.stream.data;
+  data[0].len = 971;
+  data[0].base = nulldata;
+
+  ngtcp2_strm_streamfrq_push(&strm, frc);
+
+  ngtcp2_strm_ack_data(&strm, 0, 443 + 971);
+
+  CU_ASSERT((uint64_t)-1 == ngtcp2_strm_streamfrq_unacked_offset(&strm));
+
+  ngtcp2_strm_free(&strm);
+
+  /* Everything acknowledged but fin */
+  ngtcp2_strm_init(&strm, 0, NGTCP2_STRM_FLAG_NONE, 0, 0, NULL, mem);
+
+  ngtcp2_frame_chain_new(&frc, mem);
+  frc->fr.stream.type = NGTCP2_FRAME_STREAM;
+  frc->fr.stream.fin = 0;
+  frc->fr.stream.offset = 0;
+  frc->fr.stream.datacnt = 1;
+  data = frc->fr.stream.data;
+  data[0].len = 17;
+  data[0].base = nulldata;
+
+  ngtcp2_strm_streamfrq_push(&strm, frc);
+
+  ngtcp2_frame_chain_new(&frc, mem);
+  frc->fr.stream.type = NGTCP2_FRAME_STREAM;
+  frc->fr.stream.fin = 1;
+  frc->fr.stream.offset = 443;
+  frc->fr.stream.datacnt = 1;
+  data = frc->fr.stream.data;
+  data[0].len = 971;
+  data[0].base = nulldata;
+
+  ngtcp2_strm_streamfrq_push(&strm, frc);
+
+  ngtcp2_strm_ack_data(&strm, 0, 443 + 971);
+
+  CU_ASSERT(443 + 971 == ngtcp2_strm_streamfrq_unacked_offset(&strm));
+
+  ngtcp2_strm_free(&strm);
+
+  /* Unacked gap starts in the middle of stream to resend */
+  ngtcp2_strm_init(&strm, 0, NGTCP2_STRM_FLAG_NONE, 0, 0, NULL, mem);
+
+  ngtcp2_frame_chain_new(&frc, mem);
+  frc->fr.stream.type = NGTCP2_FRAME_STREAM;
+  frc->fr.stream.fin = 0;
+  frc->fr.stream.offset = 0;
+  frc->fr.stream.datacnt = 1;
+  data = frc->fr.stream.data;
+  data[0].len = 971;
+  data[0].base = nulldata;
+
+  ngtcp2_strm_streamfrq_push(&strm, frc);
+
+  ngtcp2_strm_ack_data(&strm, 0, 443);
+
+  CU_ASSERT(443 == ngtcp2_strm_streamfrq_unacked_offset(&strm));
+
+  ngtcp2_strm_free(&strm);
+
+  /* Unacked gap starts after stream to resend */
+  ngtcp2_strm_init(&strm, 0, NGTCP2_STRM_FLAG_NONE, 0, 0, NULL, mem);
+
+  ngtcp2_frame_chain_new(&frc, mem);
+  frc->fr.stream.type = NGTCP2_FRAME_STREAM;
+  frc->fr.stream.fin = 0;
+  frc->fr.stream.offset = 0;
+  frc->fr.stream.datacnt = 1;
+  data = frc->fr.stream.data;
+  data[0].len = 971;
+  data[0].base = nulldata;
+
+  ngtcp2_strm_streamfrq_push(&strm, frc);
+
+  ngtcp2_strm_ack_data(&strm, 0, 971);
+
+  CU_ASSERT((uint64_t)-1 == ngtcp2_strm_streamfrq_unacked_offset(&strm));
+
+  ngtcp2_strm_free(&strm);
+
+  /* Unacked gap and stream overlap and gap starts before stream */
+  ngtcp2_strm_init(&strm, 0, NGTCP2_STRM_FLAG_NONE, 0, 0, NULL, mem);
+
+  ngtcp2_frame_chain_new(&frc, mem);
+  frc->fr.stream.type = NGTCP2_FRAME_STREAM;
+  frc->fr.stream.fin = 0;
+  frc->fr.stream.offset = 977;
+  frc->fr.stream.datacnt = 1;
+  data = frc->fr.stream.data;
+  data[0].len = 971;
+  data[0].base = nulldata;
+
+  ngtcp2_strm_streamfrq_push(&strm, frc);
+
+  ngtcp2_strm_ack_data(&strm, 0, 971);
+
+  CU_ASSERT(977 == ngtcp2_strm_streamfrq_unacked_offset(&strm));
+
+  ngtcp2_strm_free(&strm);
+}
+
+void test_ngtcp2_strm_streamfrq_unacked_pop(void) {
+  ngtcp2_strm strm;
+  ngtcp2_frame_chain *frc;
+  const ngtcp2_mem *mem = ngtcp2_mem_default();
+  ngtcp2_vec *data;
+  int rv;
+
+  /* Everything acknowledged including fin */
+  ngtcp2_strm_init(&strm, 0, NGTCP2_STRM_FLAG_FIN_ACKED, 0, 0, NULL, mem);
+
+  ngtcp2_frame_chain_new(&frc, mem);
+  frc->fr.stream.type = NGTCP2_FRAME_STREAM;
+  frc->fr.stream.fin = 0;
+  frc->fr.stream.offset = 307;
+  frc->fr.stream.datacnt = 1;
+  data = frc->fr.stream.data;
+  data[0].len = 149;
+  data[0].base = nulldata;
+
+  ngtcp2_strm_streamfrq_push(&strm, frc);
+
+  ngtcp2_frame_chain_new(&frc, mem);
+  frc->fr.stream.type = NGTCP2_FRAME_STREAM;
+  frc->fr.stream.fin = 1;
+  frc->fr.stream.offset = 457;
+  frc->fr.stream.datacnt = 1;
+  data = frc->fr.stream.data;
+  data[0].len = 307;
+  data[0].base = nulldata;
+
+  ngtcp2_strm_streamfrq_push(&strm, frc);
+
+  ngtcp2_strm_ack_data(&strm, 0, 764);
+
+  frc = NULL;
+  rv = ngtcp2_strm_streamfrq_pop(&strm, &frc, 1024);
+
+  CU_ASSERT(0 == rv);
+  CU_ASSERT(NULL == frc);
+
+  ngtcp2_strm_free(&strm);
+
+  /* Everything acknowledged but fin */
+  ngtcp2_strm_init(&strm, 0, NGTCP2_STRM_FLAG_NONE, 0, 0, NULL, mem);
+
+  ngtcp2_frame_chain_new(&frc, mem);
+  frc->fr.stream.type = NGTCP2_FRAME_STREAM;
+  frc->fr.stream.fin = 0;
+  frc->fr.stream.offset = 307;
+  frc->fr.stream.datacnt = 1;
+  data = frc->fr.stream.data;
+  data[0].len = 149;
+  data[0].base = nulldata;
+
+  ngtcp2_strm_streamfrq_push(&strm, frc);
+
+  ngtcp2_frame_chain_new(&frc, mem);
+  frc->fr.stream.type = NGTCP2_FRAME_STREAM;
+  frc->fr.stream.fin = 1;
+  frc->fr.stream.offset = 457;
+  frc->fr.stream.datacnt = 1;
+  data = frc->fr.stream.data;
+  data[0].len = 307;
+  data[0].base = nulldata;
+
+  ngtcp2_strm_streamfrq_push(&strm, frc);
+
+  ngtcp2_strm_ack_data(&strm, 0, 764);
+
+  frc = NULL;
+  rv = ngtcp2_strm_streamfrq_pop(&strm, &frc, 1024);
+
+  CU_ASSERT(0 == rv);
+  CU_ASSERT(1 == frc->fr.stream.fin);
+  CU_ASSERT(764 == frc->fr.stream.offset);
+  CU_ASSERT(0 == ngtcp2_vec_len(frc->fr.stream.data, frc->fr.stream.datacnt));
+
+  ngtcp2_frame_chain_del(frc, mem);
+  ngtcp2_strm_free(&strm);
+
+  /* Remove leading acknowledged data */
+  setup_strm_streamfrq_fixture(&strm, mem);
+
+  ngtcp2_strm_ack_data(&strm, 0, 12);
+
+  rv = ngtcp2_strm_streamfrq_pop(&strm, &frc, 43);
+
+  CU_ASSERT(0 == rv);
+  CU_ASSERT(0 == frc->fr.stream.fin);
+  CU_ASSERT(12 == frc->fr.stream.offset);
+  CU_ASSERT(1 == frc->fr.stream.datacnt);
+  CU_ASSERT(43 == ngtcp2_vec_len(frc->fr.stream.data, frc->fr.stream.datacnt));
+
+  ngtcp2_frame_chain_del(frc, mem);
+  ngtcp2_strm_free(&strm);
+
+  /* Creating a gap of acknowledged data */
+  setup_strm_streamfrq_fixture(&strm, mem);
+
+  ngtcp2_strm_ack_data(&strm, 32, 1);
+
+  rv = ngtcp2_strm_streamfrq_pop(&strm, &frc, 43);
+
+  CU_ASSERT(0 == rv);
+  CU_ASSERT(0 == frc->fr.stream.fin);
+  CU_ASSERT(0 == frc->fr.stream.offset);
+  CU_ASSERT(2 == frc->fr.stream.datacnt);
+  CU_ASSERT(32 == ngtcp2_vec_len(frc->fr.stream.data, frc->fr.stream.datacnt));
+
+  ngtcp2_frame_chain_del(frc, mem);
+  ngtcp2_strm_free(&strm);
+}
diff --git a/tests/ngtcp2_strm_test.h b/tests/ngtcp2_strm_test.h
index abc9ebbbe0a867f62b1ddb63661504b73896452c..f94c4238b76a3dbf46f49999528aab5e91c1c7fc 100644
--- a/tests/ngtcp2_strm_test.h
+++ b/tests/ngtcp2_strm_test.h
@@ -30,5 +30,7 @@
 #endif /* HAVE_CONFIG_H */
 
 void test_ngtcp2_strm_streamfrq_pop(void);
+void test_ngtcp2_strm_streamfrq_unacked_offset(void);
+void test_ngtcp2_strm_streamfrq_unacked_pop(void);
 
 #endif /* NGTCP2_STRM_TEST_H */