diff --git a/doc/programmers-guide.rst b/doc/programmers-guide.rst
index 8a4bed47b485ce51b674d656d7745a9eb4533a91..4ba4f69a96c2b105e4b40d87802998984588c6c3 100644
--- a/doc/programmers-guide.rst
+++ b/doc/programmers-guide.rst
@@ -177,8 +177,8 @@ client_*_traffic_secret.
 After Handshake key is available, set AEAD overhead (tag length) using
 `ngtcp2_conn_set_aead_overhead()` function.
 
-`ngtcp2_conn_write_handshake()` initiates QUIC handshake.  The Initial
-keys must be installed before calling this function.
+`ngtcp2_conn_write_pkt()` initiates QUIC handshake.  The Initial keys
+must be installed before calling this function.
 
 For client application, it first calls
 ``ngtcp2_conn_callbacks.client_initial`` callback.  The callback must
@@ -198,7 +198,7 @@ After negotiated Handshake keys are available,
 negotiated cipher suites.  If ChaCha20 based cipher suite is
 negotiated, ChaCha20 is used to protect packet header.
 
-`ngtcp2_conn_read_handshake()` reads QUIC handshake packets.
+`ngtcp2_conn_read_pkt()` reads QUIC handshake packets.
 
 For server application, it first calls
 ``ngtcp2_conn_callbacks.recv_client_initial`` callback.  The callback
@@ -218,23 +218,23 @@ acknowledges TLS messages,
 ``ngtcp2_conn_callbacks.acked_crypto_offset`` callback is called.  The
 application can throw away data acknowledged.
 
-`ngtcp2_conn_read_handshake()` and `ngtcp2_conn_write_handshake()`
-should be called until `ngtcp2_conn_get_handshake_completed()` returns
-nonzero which means QUIC handshake has completed.
+`ngtcp2_conn_read_pkt()` and `ngtcp2_conn_write_pkt()` performs QUIC
+handshake until `ngtcp2_conn_get_handshake_completed()` returns
+nonzero which means QUIC handshake has completed.  They can be used
+for post-handshake data transfer as well.  To send stream data, use
+`ngtcp2_conn_writev_stream()`.
 
 0RTT data transmission
 ----------------------
 
 In order for client to send 0RTT data, it should use
-`ngtcp2_conn_client_write_handshake()` function instead of
-`ngtcp2_conn_write_handshake()`.
-`ngtcp2_conn_client_write_handshake()` accepts 0RTT data to send.
+`ngtcp2_conn_writev_stream()` function.
 
 Client application has to load resumed TLS session.  It also has to
 set the remembered transport parameter using
 `ngtcp2_conn_set_early_remote_transport_params()` function.
 
-Before calling `ngtcp2_conn_client_write_handshake()`, client
+Before calling `ngtcp2_conn_client_writev_stream()`, client
 application has to open stream to send data using
 `ngtcp2_conn_open_bidi_stream()` (or `ngtcp2_conn_open_uni_stream()`
 for unidirectional stream).
@@ -268,28 +268,27 @@ When timer fires, it has to call some API functions.  If the current
 timestamp is equal to or larger than the value returned from
 `ngtcp2_conn_loss_detection_expiry()`, it has to call
 `ngtcp2_conn_on_loss_detection_timer()` and `ngtcp2_conn_write_pkt()`
-(or `ngtcp2_conn_write_handshake()` if handshake has not completed
-yet).  If the current timestamp is equal to or larger than the value
-returned from `ngtcp2_conn_ack_delay_expiry()`, it has to call
+(or `ngtcp2_conn_writev_stream()`).  If the current timestamp is equal
+to or larger than the value returned from
+`ngtcp2_conn_ack_delay_expiry()`, it has to call
 `ngtcp2_conn_cancel_expired_ack_delay_timer()` and
-`ngtcp2_conn_write_pkt()` (or `ngtcp2_conn_write_handshake()` if
-handshake has not completed yet).  After calling these functions, new
-expiry will be set.  The application should call
-`ngtcp2_conn_get_expiry()` to restart timer.
+`ngtcp2_conn_write_pkt()` (or `ngtcp2_conn_writev_stream()`).  After
+calling these functions, new expiry will be set.  The application
+should call `ngtcp2_conn_get_expiry()` to restart timer.
 
 
 After QUIC handshake
 --------------------
 
-After QUIC handshake completed, call `ngtcp2_conn_read_pkt()` to read
+After QUIC handshake completed, `ngtcp2_conn_read_pkt()` can read
 incoming QUIC packets.  To write QUIC packets, call
 `ngtcp2_conn_write_pkt()`.
 
 In order to send stream data, the application has to first open a
 stream.  Use `ngtcp2_conn_open_bidi_stream()` to open bidirectional
 stream.  For unidirectional stream, call
-`ngtcp2_conn_open_uni_stream()`.  Call `ngtcp2_conn_write_stream()` to
-send stream data.
+`ngtcp2_conn_open_uni_stream()`.  Call `ngtcp2_conn_writev_stream()`
+to send stream data.
 
 Closing connection
 ------------------
diff --git a/examples/client.cc b/examples/client.cc
index 7cf29c3c6db1008c108048810ee7e5a9e17c29aa..8271f3736a0619b979c557f80c9a336a81f84b7a 100644
--- a/examples/client.cc
+++ b/examples/client.cc
@@ -1273,102 +1273,19 @@ int Client::feed_data(const sockaddr *sa, socklen_t salen, uint8_t *data,
   auto path = ngtcp2_path{
       {local_addr_.len, reinterpret_cast<uint8_t *>(&local_addr_.su)},
       {salen, const_cast<uint8_t *>(reinterpret_cast<const uint8_t *>(sa))}};
-  if (ngtcp2_conn_get_handshake_completed(conn_)) {
-    rv = ngtcp2_conn_read_pkt(conn_, &path, data, datalen,
-                              util::timestamp(loop_));
-    if (rv != 0) {
-      std::cerr << "ngtcp2_conn_read_pkt: " << ngtcp2_strerror(rv) << std::endl;
-      if (!last_error_.code) {
-        last_error_ = quicErrorTransport(rv);
-      }
-      disconnect();
-      return -1;
+  rv =
+      ngtcp2_conn_read_pkt(conn_, &path, data, datalen, util::timestamp(loop_));
+  if (rv != 0) {
+    std::cerr << "ngtcp2_conn_read_pkt: " << ngtcp2_strerror(rv) << std::endl;
+    if (!last_error_.code) {
+      last_error_ = quicErrorTransport(rv);
     }
-    return 0;
-  }
-
-  return do_handshake(&path, data, datalen);
-}
-
-int Client::do_handshake_read_once(const ngtcp2_path *path, const uint8_t *data,
-                                   size_t datalen) {
-  auto rv = ngtcp2_conn_read_handshake(conn_, path, data, datalen,
-                                       util::timestamp(loop_));
-  if (rv < 0) {
-    std::cerr << "ngtcp2_conn_read_handshake: " << ngtcp2_strerror(rv)
-              << std::endl;
-    last_error_ = quicErrorTransport(rv);
     disconnect();
     return -1;
   }
-
   return 0;
 }
 
-ssize_t Client::do_handshake_write_once() {
-  auto nwrite = ngtcp2_conn_write_handshake(conn_, sendbuf_.wpos(), max_pktlen_,
-                                            util::timestamp(loop_));
-  if (nwrite < 0) {
-    std::cerr << "ngtcp2_conn_write_handshake: " << ngtcp2_strerror(nwrite)
-              << std::endl;
-    last_error_ = quicErrorTransport(nwrite);
-    disconnect();
-    return -1;
-  }
-
-  if (nwrite == 0) {
-    return 0;
-  }
-
-  sendbuf_.push(nwrite);
-
-  auto rv = send_packet();
-  if (rv == NETWORK_ERR_SEND_BLOCKED) {
-    schedule_retransmit();
-    return rv;
-  }
-  if (rv != NETWORK_ERR_OK) {
-    return rv;
-  }
-
-  return nwrite;
-}
-
-int Client::do_handshake(const ngtcp2_path *path, const uint8_t *data,
-                         size_t datalen) {
-  ssize_t nwrite;
-
-  if (sendbuf_.size() > 0) {
-    auto rv = send_packet();
-    if (rv != NETWORK_ERR_OK) {
-      return rv;
-    }
-  }
-
-  if (datalen) {
-    auto rv = do_handshake_read_once(path, data, datalen);
-    if (rv != 0) {
-      return rv;
-    }
-  }
-
-  // For 0-RTT
-  auto rv = write_0rtt_streams();
-  if (rv != 0) {
-    return rv;
-  }
-
-  for (;;) {
-    nwrite = do_handshake_write_once();
-    if (nwrite < 0) {
-      return nwrite;
-    }
-    if (nwrite == 0) {
-      return 0;
-    }
-  }
-}
-
 int Client::on_read() {
   std::array<uint8_t, 65536> buf;
   sockaddr_union su;
@@ -1440,49 +1357,12 @@ int Client::on_write(bool retransmit) {
     }
   }
 
-  if (!ngtcp2_conn_get_handshake_completed(conn_)) {
-    auto rv = do_handshake(nullptr, nullptr, 0);
-    schedule_retransmit();
-    return rv;
-  }
-
-  PathStorage path;
-
-  for (;;) {
-    auto n = ngtcp2_conn_write_pkt(conn_, &path.path, sendbuf_.wpos(),
-                                   max_pktlen_, util::timestamp(loop_));
-    if (n < 0) {
-      std::cerr << "ngtcp2_conn_write_pkt: " << ngtcp2_strerror(n) << std::endl;
-      last_error_ = quicErrorTransport(n);
-      disconnect();
-      return -1;
-    }
-    if (n == 0) {
-      break;
-    }
-
-    sendbuf_.push(n);
-
-    update_remote_addr(&path.path.remote);
-
-    auto rv = send_packet();
+  auto rv = write_streams();
+  if (rv != 0) {
     if (rv == NETWORK_ERR_SEND_BLOCKED) {
       schedule_retransmit();
-      return rv;
-    }
-    if (rv != NETWORK_ERR_OK) {
-      return rv;
-    }
-  }
-
-  if (!retransmit) {
-    auto rv = write_streams();
-    if (rv != 0) {
-      if (rv == NETWORK_ERR_SEND_BLOCKED) {
-        schedule_retransmit();
-      }
-      return rv;
     }
+    return rv;
   }
 
   schedule_retransmit();
@@ -1493,146 +1373,60 @@ int Client::write_streams() {
   std::array<nghttp3_vec, 16> vec;
   int rv;
 
-  if (ngtcp2_conn_get_max_data_left(conn_) == 0) {
-    return 0;
-  }
-
   for (;;) {
     int64_t stream_id;
     int fin;
 
-    auto sveccnt = nghttp3_conn_writev_stream(httpconn_, &stream_id, &fin,
-                                              vec.data(), vec.size());
-    if (sveccnt < 0) {
-      std::cerr << "nghttp3_conn_writev_stream: " << nghttp3_strerror(sveccnt)
-                << std::endl;
-      last_error_ = quicErrorApplication(sveccnt);
-      disconnect();
-      return -1;
-    }
+    ssize_t sveccnt = 0;
 
-    if (sveccnt == 0) {
-      break;
-    }
-
-    ssize_t ndatalen;
-    PathStorage path;
-    auto v = vec.data();
-    auto vcnt = static_cast<size_t>(sveccnt);
-    for (;;) {
-      auto nwrite = ngtcp2_conn_writev_stream(
-          conn_, &path.path, sendbuf_.wpos(), max_pktlen_, &ndatalen, stream_id,
-          fin, reinterpret_cast<const ngtcp2_vec *>(v), vcnt,
-          util::timestamp(loop_));
-      if (nwrite < 0) {
-        auto should_break = false;
-        switch (nwrite) {
-        case NGTCP2_ERR_STREAM_DATA_BLOCKED:
-          if (ngtcp2_conn_get_max_data_left(conn_) == 0) {
-            return 0;
-          }
-
-          rv = nghttp3_conn_block_stream(httpconn_, stream_id);
-          if (rv != 0) {
-            std::cerr << "nghttp3_conn_block_stream: " << nghttp3_strerror(rv)
-                      << std::endl;
-            last_error_ = quicErrorApplication(rv);
-            disconnect();
-            return -1;
-          }
-          should_break = true;
-          break;
-        case NGTCP2_ERR_EARLY_DATA_REJECTED:
-        case NGTCP2_ERR_STREAM_SHUT_WR:
-        case NGTCP2_ERR_STREAM_NOT_FOUND: // This means that stream is
-                                          // closed.
-          should_break = true;
-          break;
-        }
-
-        if (should_break) {
-          break;
-        }
-
-        std::cerr << "ngtcp2_conn_write_stream: " << ngtcp2_strerror(nwrite)
+    if (httpconn_ && ngtcp2_conn_get_max_data_left(conn_)) {
+      sveccnt = nghttp3_conn_writev_stream(httpconn_, &stream_id, &fin,
+                                           vec.data(), vec.size());
+      if (sveccnt < 0) {
+        std::cerr << "nghttp3_conn_writev_stream: " << nghttp3_strerror(sveccnt)
                   << std::endl;
-        last_error_ = quicErrorTransport(nwrite);
+        last_error_ = quicErrorApplication(sveccnt);
         disconnect();
         return -1;
       }
+    }
 
-      if (nwrite == 0) {
-        // We are congestion limited.
-        return 0;
-      }
-
-      sendbuf_.push(nwrite);
-
-      if (ndatalen > 0) {
-        rv = nghttp3_conn_add_write_offset(httpconn_, stream_id, ndatalen);
-        if (rv != 0) {
-          std::cerr << "nghttp3_conn_add_write_offset: " << nghttp3_strerror(rv)
+    ssize_t ndatalen;
+    PathStorage path;
+    if (sveccnt == 0) {
+      for (;;) {
+        auto nwrite =
+            ngtcp2_conn_write_pkt(conn_, &path.path, sendbuf_.wpos(),
+                                  max_pktlen_, util::timestamp(loop_));
+        if (nwrite < 0) {
+          std::cerr << "ngtcp2_conn_write_pkt: " << ngtcp2_strerror(nwrite)
                     << std::endl;
-          last_error_ = quicErrorApplication(rv);
+          last_error_ = quicErrorTransport(nwrite);
           disconnect();
           return -1;
         }
+        if (nwrite == 0) {
+          return 0;
+        }
+        sendbuf_.push(nwrite);
 
-        nghttp3_vec_consume(&v, &vcnt, ndatalen);
-      }
-
-      update_remote_addr(&path.path.remote);
+        update_remote_addr(&path.path.remote);
 
-      auto rv = send_packet();
-      if (rv != NETWORK_ERR_OK) {
-        return rv;
-      }
+        auto rv = send_packet();
+        if (rv != NETWORK_ERR_OK) {
+          return rv;
+        }
 
-      if (nghttp3_vec_empty(v, vcnt)) {
-        break;
+        return 0;
       }
     }
-  }
-
-  return 0;
-}
-
-int Client::write_0rtt_streams() {
-  if (!httpconn_) {
-    return 0;
-  }
 
-  std::array<nghttp3_vec, 16> vec;
-  int rv;
-
-  if (ngtcp2_conn_get_max_data_left(conn_) == 0) {
-    return 0;
-  }
-
-  for (;;) {
-    int64_t stream_id;
-    int fin;
-
-    auto sveccnt = nghttp3_conn_writev_stream(httpconn_, &stream_id, &fin,
-                                              vec.data(), vec.size());
-    if (sveccnt < 0) {
-      std::cerr << "nghttp3_conn_writev_stream: " << nghttp3_strerror(sveccnt)
-                << std::endl;
-      last_error_ = quicErrorApplication(sveccnt);
-      disconnect();
-      return -1;
-    }
-
-    if (sveccnt == 0) {
-      break;
-    }
-
-    ssize_t ndatalen;
     auto v = vec.data();
     auto vcnt = static_cast<size_t>(sveccnt);
     for (;;) {
-      auto nwrite = ngtcp2_conn_client_write_handshake(
-          conn_, sendbuf_.wpos(), max_pktlen_, &ndatalen, stream_id, fin,
+      auto nwrite = ngtcp2_conn_writev_stream(
+          conn_, &path.path, sendbuf_.wpos(), max_pktlen_, &ndatalen,
+          NGTCP2_WRITE_STREAM_FLAG_MORE, stream_id, fin,
           reinterpret_cast<const ngtcp2_vec *>(v), vcnt,
           util::timestamp(loop_));
       if (nwrite < 0) {
@@ -1659,6 +1453,18 @@ int Client::write_0rtt_streams() {
                                           // closed.
           should_break = true;
           break;
+        case NGTCP2_ERR_WRITE_STREAM_MORE:
+          assert(ndatalen > 0);
+          rv = nghttp3_conn_add_write_offset(httpconn_, stream_id, ndatalen);
+          if (rv != 0) {
+            std::cerr << "nghttp3_conn_add_write_offset: "
+                      << nghttp3_strerror(rv) << std::endl;
+            last_error_ = quicErrorApplication(rv);
+            disconnect();
+            return -1;
+          }
+          should_break = true;
+          break;
         }
 
         if (should_break) {
@@ -1692,6 +1498,8 @@ int Client::write_0rtt_streams() {
         nghttp3_vec_consume(&v, &vcnt, ndatalen);
       }
 
+      update_remote_addr(&path.path.remote);
+
       auto rv = send_packet();
       if (rv != NETWORK_ERR_OK) {
         return rv;
@@ -1702,8 +1510,6 @@ int Client::write_0rtt_streams() {
       }
     }
   }
-
-  return 0;
 }
 
 void Client::schedule_retransmit() {
@@ -2887,7 +2693,6 @@ SSL_CTX *create_ssl_ctx() {
 namespace {
 int run(Client &c, const char *addr, const char *port) {
   Address remote_addr, local_addr;
-  ssize_t nwrite;
 
   auto fd = create_sock(remote_addr, addr, port);
   if (fd == -1) {
@@ -2914,19 +2719,12 @@ int run(Client &c, const char *addr, const char *port) {
     }
   }
 
-  // For 0-RTT
-  auto rv = c.write_0rtt_streams();
+  // TODO Do we need this ?
+  auto rv = c.on_write();
   if (rv != 0) {
     return rv;
   }
 
-  nwrite = c.do_handshake_write_once();
-  if (nwrite < 0) {
-    return nwrite;
-  }
-
-  c.schedule_retransmit();
-
   ev_run(EV_DEFAULT, 0);
 
   return 0;
diff --git a/examples/client.h b/examples/client.h
index 336aca42cd2daab848bdfdcab49bef48d2512ae3..5ad71d9081e2ebc728f061a3620d12dc5ad40ae0 100644
--- a/examples/client.h
+++ b/examples/client.h
@@ -164,14 +164,8 @@ public:
   int on_read();
   int on_write(bool retransmit = false);
   int write_streams();
-  int write_0rtt_streams();
   int feed_data(const sockaddr *sa, socklen_t salen, uint8_t *data,
                 size_t datalen);
-  int do_handshake(const ngtcp2_path *path, const uint8_t *data,
-                   size_t datalen);
-  int do_handshake_read_once(const ngtcp2_path *path, const uint8_t *data,
-                             size_t datalen);
-  ssize_t do_handshake_write_once();
   void schedule_retransmit();
   int handshake_completed();
 
diff --git a/examples/server.cc b/examples/server.cc
index 660999279a28f27bdf1850b07d03afe6126deaa8..2fe1960e7ca8f62e2b21015b7bef5e5512f6a696 100644
--- a/examples/server.cc
+++ b/examples/server.cc
@@ -1993,77 +1993,6 @@ ssize_t Handler::hp_mask(uint8_t *dest, size_t destlen, const uint8_t *key,
                          samplelen);
 }
 
-int Handler::do_handshake_read_once(const ngtcp2_path *path,
-                                    const uint8_t *data, size_t datalen) {
-  auto rv = ngtcp2_conn_read_handshake(conn_, path, data, datalen,
-                                       util::timestamp(loop_));
-  if (rv != 0) {
-    std::cerr << "ngtcp2_conn_read_handshake: " << ngtcp2_strerror(rv)
-              << std::endl;
-    if (!last_error_.code) {
-      last_error_ = quicErrorTransport(rv);
-    }
-    return -1;
-  }
-  return 0;
-}
-
-ssize_t Handler::do_handshake_write_once() {
-  auto nwrite = ngtcp2_conn_write_handshake(conn_, sendbuf_.wpos(), max_pktlen_,
-                                            util::timestamp(loop_));
-  if (nwrite < 0) {
-    std::cerr << "ngtcp2_conn_write_handshake: " << ngtcp2_strerror(nwrite)
-              << std::endl;
-    last_error_ = quicErrorTransport(nwrite);
-    return -1;
-  }
-
-  if (nwrite == 0) {
-    return 0;
-  }
-
-  sendbuf_.push(nwrite);
-
-  auto rv = server_->send_packet(*endpoint_, remote_addr_, sendbuf_, &wev_);
-  if (rv == NETWORK_ERR_SEND_BLOCKED) {
-    schedule_retransmit();
-    return rv;
-  }
-  if (rv != NETWORK_ERR_OK) {
-    return rv;
-  }
-  ev_timer_again(loop_, &timer_);
-
-  return nwrite;
-}
-
-int Handler::do_handshake(const ngtcp2_path *path, const uint8_t *data,
-                          size_t datalen) {
-  if (datalen) {
-    auto rv = do_handshake_read_once(path, data, datalen);
-    if (rv != 0) {
-      return rv;
-    }
-  }
-
-  if (sendbuf_.size() > 0) {
-    auto rv = server_->send_packet(*endpoint_, remote_addr_, sendbuf_, &wev_);
-    if (rv != NETWORK_ERR_OK) {
-      return rv;
-    }
-  }
-
-  for (;;) {
-    auto nwrite = do_handshake_write_once();
-    if (nwrite < 0) {
-      return nwrite;
-    }
-    if (nwrite == 0) {
-      return 0;
-    }
-  }
-}
-
 void Handler::update_endpoint(const ngtcp2_addr *addr) {
   endpoint_ = static_cast<Endpoint *>(addr->user_data);
   assert(endpoint_);
@@ -2084,26 +2013,17 @@ int Handler::feed_data(const Endpoint &ep, const sockaddr *sa, socklen_t salen,
        const_cast<Endpoint *>(&ep)},
       {salen, const_cast<uint8_t *>(reinterpret_cast<const uint8_t *>(sa))}};
 
-  if (ngtcp2_conn_get_handshake_completed(conn_)) {
-    rv = ngtcp2_conn_read_pkt(conn_, &path, data, datalen,
-                              util::timestamp(loop_));
-    if (rv != 0) {
-      std::cerr << "ngtcp2_conn_read_pkt: " << ngtcp2_strerror(rv) << std::endl;
-      if (rv == NGTCP2_ERR_DRAINING) {
-        start_draining_period();
-        return NETWORK_ERR_CLOSE_WAIT;
-      }
-      if (!last_error_.code) {
-        last_error_ = quicErrorTransport(rv);
-      }
-      return handle_error();
-    }
-
-    return 0;
-  }
-
-  rv = do_handshake(&path, data, datalen);
+  rv =
+      ngtcp2_conn_read_pkt(conn_, &path, data, datalen, util::timestamp(loop_));
   if (rv != 0) {
+    std::cerr << "ngtcp2_conn_read_pkt: " << ngtcp2_strerror(rv) << std::endl;
+    if (rv == NGTCP2_ERR_DRAINING) {
+      start_draining_period();
+      return NETWORK_ERR_CLOSE_WAIT;
+    }
+    if (!last_error_.code) {
+      last_error_ = quicErrorTransport(rv);
+    }
     return handle_error();
   }
 
@@ -2146,16 +2066,6 @@ int Handler::on_write() {
 
   assert(sendbuf_.left() >= max_pktlen_);
 
-  if (!ngtcp2_conn_get_handshake_completed(conn_)) {
-    rv = do_handshake(nullptr, nullptr, 0);
-    if (rv == NETWORK_ERR_SEND_BLOCKED) {
-      schedule_retransmit();
-    }
-    if (rv != NETWORK_ERR_OK) {
-      return rv;
-    }
-  }
-
   rv = write_streams();
   if (rv != 0) {
     if (rv == NETWORK_ERR_SEND_BLOCKED) {
@@ -2164,82 +2074,71 @@ int Handler::on_write() {
     return rv;
   }
 
-  if (!ngtcp2_conn_get_handshake_completed(conn_)) {
-    schedule_retransmit();
-    return 0;
-  }
-
-  PathStorage path;
-
-  for (;;) {
-    auto n = ngtcp2_conn_write_pkt(conn_, &path.path, sendbuf_.wpos(),
-                                   max_pktlen_, util::timestamp(loop_));
-    if (n < 0) {
-      std::cerr << "ngtcp2_conn_write_pkt: " << ngtcp2_strerror(n) << std::endl;
-      last_error_ = quicErrorTransport(n);
-      return handle_error();
-    }
-    if (n == 0) {
-      break;
-    }
-
-    sendbuf_.push(n);
-
-    update_endpoint(&path.path.local);
-    update_remote_addr(&path.path.remote);
-
-    auto rv = server_->send_packet(*endpoint_, remote_addr_, sendbuf_, &wev_);
-    if (rv == NETWORK_ERR_SEND_BLOCKED) {
-      schedule_retransmit();
-      return rv;
-    }
-    if (rv != NETWORK_ERR_OK) {
-      return rv;
-    }
-    reset_idle_timer();
-  }
-
   schedule_retransmit();
+
   return 0;
 }
 
 int Handler::write_streams() {
-  if (!httpconn_) {
-    return 0;
-  }
-
   std::array<nghttp3_vec, 16> vec;
   int rv;
 
-  if (ngtcp2_conn_get_max_data_left(conn_) == 0) {
-    return 0;
-  }
-
   for (;;) {
     int64_t stream_id;
     int fin;
 
-    auto sveccnt = nghttp3_conn_writev_stream(httpconn_, &stream_id, &fin,
-                                              vec.data(), vec.size());
-    if (sveccnt < 0) {
-      std::cerr << "nghttp3_conn_writev_stream: " << nghttp3_strerror(sveccnt)
-                << std::endl;
-      last_error_ = quicErrorApplication(sveccnt);
-      return handle_error();
-    }
+    ssize_t sveccnt = 0;
 
-    if (sveccnt == 0) {
-      break;
+    if (httpconn_ && ngtcp2_conn_get_max_data_left(conn_)) {
+      sveccnt = nghttp3_conn_writev_stream(httpconn_, &stream_id, &fin,
+                                           vec.data(), vec.size());
+      if (sveccnt < 0) {
+        std::cerr << "nghttp3_conn_writev_stream: " << nghttp3_strerror(sveccnt)
+                  << std::endl;
+        last_error_ = quicErrorApplication(sveccnt);
+        return handle_error();
+      }
     }
 
     ssize_t ndatalen;
     PathStorage path;
+    if (sveccnt == 0) {
+      for (;;) {
+        auto nwrite =
+            ngtcp2_conn_write_pkt(conn_, &path.path, sendbuf_.wpos(),
+                                  max_pktlen_, util::timestamp(loop_));
+        if (nwrite < 0) {
+          std::cerr << "ngtcp2_conn_write_pkt: " << ngtcp2_strerror(nwrite)
+                    << std::endl;
+          last_error_ = quicErrorTransport(nwrite);
+          return handle_error();
+        }
+        if (nwrite == 0) {
+          return 0;
+        }
+        sendbuf_.push(nwrite);
+
+        update_endpoint(&path.path.local);
+        update_remote_addr(&path.path.remote);
+
+        auto rv =
+            server_->send_packet(*endpoint_, remote_addr_, sendbuf_, &wev_);
+        if (rv != NETWORK_ERR_OK) {
+          return rv;
+        }
+        reset_idle_timer();
+
+        return 0;
+      }
+    }
+
     auto v = vec.data();
     auto vcnt = static_cast<size_t>(sveccnt);
     for (;;) {
       auto nwrite = ngtcp2_conn_writev_stream(
-          conn_, &path.path, sendbuf_.wpos(), max_pktlen_, &ndatalen, stream_id,
-          fin, reinterpret_cast<const ngtcp2_vec *>(v), vcnt,
+          conn_, &path.path, sendbuf_.wpos(), max_pktlen_, &ndatalen,
+          NGTCP2_WRITE_STREAM_FLAG_MORE, stream_id, fin,
+          reinterpret_cast<const ngtcp2_vec *>(v), vcnt,
           util::timestamp(loop_));
       if (nwrite < 0) {
         auto should_break = false;
@@ -2261,6 +2160,17 @@ int Handler::write_streams() {
         case NGTCP2_ERR_STREAM_SHUT_WR:
           should_break = true;
           break;
+        case NGTCP2_ERR_WRITE_STREAM_MORE:
+          assert(ndatalen > 0);
+          rv = nghttp3_conn_add_write_offset(httpconn_, stream_id, ndatalen);
+          if (rv != 0) {
+            std::cerr << "nghttp3_conn_add_write_offset: "
+                      << nghttp3_strerror(rv) << std::endl;
+            last_error_ = quicErrorApplication(rv);
+            return handle_error();
+          }
+          should_break = true;
+          break;
         }
 
         if (should_break) {
@@ -2312,8 +2222,6 @@ int Handler::write_streams() {
       }
     }
   }
-
-  return 0;
 }
 
 bool Handler::draining() const { return draining_; }
diff --git a/examples/server.h b/examples/server.h
index c5d60a189123811ca7ff34611a69e7ea6dcffc34..6ad960ad4785e6f811e65b9aa17eb82227310731 100644
--- a/examples/server.h
+++ b/examples/server.h
@@ -194,11 +194,6 @@ public:
   int write_streams();
   int feed_data(const Endpoint &ep, const sockaddr *sa, socklen_t salen,
                 uint8_t *data, size_t datalen);
-  int do_handshake_read_once(const ngtcp2_path *path, const uint8_t *data,
-                             size_t datalen);
-  ssize_t do_handshake_write_once();
-  int do_handshake(const ngtcp2_path *path, const uint8_t *data,
-                   size_t datalen);
   void schedule_retransmit();
   void signal_write();
   int handshake_completed();
diff --git a/lib/includes/ngtcp2/ngtcp2.h b/lib/includes/ngtcp2/ngtcp2.h
index 8b6bb4c9995ea2151a9d71cbc1a0f8b281746077..a8200e097dfeaf32a571c4c0ee58cd4b323df64f 100644
--- a/lib/includes/ngtcp2/ngtcp2.h
+++ b/lib/includes/ngtcp2/ngtcp2.h
@@ -241,6 +241,7 @@ typedef enum {
   NGTCP2_ERR_CONN_ID_BLOCKED = -237,
   NGTCP2_ERR_INTERNAL = -238,
   NGTCP2_ERR_CRYPTO_BUFFER_EXCEEDED = -239,
+  NGTCP2_ERR_WRITE_STREAM_MORE = -240,
   NGTCP2_ERR_FATAL = -500,
   NGTCP2_ERR_NOMEM = -501,
   NGTCP2_ERR_CALLBACK_FAILURE = -502,
@@ -1585,110 +1586,13 @@ ngtcp2_conn_server_new(ngtcp2_conn **pconn, const ngtcp2_cid *dcid,
  */
 NGTCP2_EXTERN void ngtcp2_conn_del(ngtcp2_conn *conn);
 
-/**
- * @function
- *
- * `ngtcp2_conn_read_handshake` performs QUIC cryptographic handshake
- * by reading given data.  |pkt| points to the buffer to read and
- * |pktlen| is the length of the buffer.  |path| is the network path.
- *
- * The application should call `ngtcp2_conn_write_handshake` (or
- * `ngtcp2_conn_client_write_handshake` for client session) to make
- * handshake go forward after calling this function.
- *
- * Application should call this function until
- * `ngtcp2_conn_get_handshake_completed` returns nonzero.  After the
- * completion of handshake, `ngtcp2_conn_read_pkt` and
- * `ngtcp2_conn_write_pkt` should be called instead.
- *
- * This function must not be called from inside the callback
- * functions.
- *
- * This function returns 0 if it succeeds, or one of the following
- * negative error codes: (TBD).
- */
-NGTCP2_EXTERN int ngtcp2_conn_read_handshake(ngtcp2_conn *conn,
-                                             const ngtcp2_path *path,
-                                             const uint8_t *pkt, size_t pktlen,
-                                             ngtcp2_tstamp ts);
-
-/**
- * @function
- *
- * `ngtcp2_conn_write_handshake` performs QUIC cryptographic handshake
- * by writing handshake packets.  It may write a packet in the given
- * buffer pointed by |dest| whose capacity is given as |destlen|.
- * Application must ensure that the buffer pointed by |dest| is not
- * empty.
- *
- * Application should keep calling this function repeatedly until it
- * returns zero, or negative error code.
- *
- * Application should call this function until
- * `ngtcp2_conn_get_handshake_completed` returns nonzero.  After the
- * completion of handshake, `ngtcp2_conn_read_pkt` and
- * `ngtcp2_conn_write_pkt` should be called instead.
- *
- * During handshake, application can send 0-RTT data (or its response)
- * using `ngtcp2_conn_write_stream`.
- * `ngtcp2_conn_client_write_handshake` is generally efficient because
- * it can coalesce Handshake packet and 0-RTT packet into one UDP
- * packet.
- *
- * This function returns 0 if it cannot write any frame because buffer
- * is too small, or packet is congestion limited.  Application should
- * keep reading and wait for congestion window to grow.
- *
- * This function must not be called from inside the callback
- * functions.
- *
- * This function returns the number of bytes written to the buffer
- * pointed by |dest| if it succeeds, or one of the following negative
- * error codes: (TBD).
- */
-NGTCP2_EXTERN ssize_t ngtcp2_conn_write_handshake(ngtcp2_conn *conn,
-                                                  uint8_t *dest, size_t destlen,
-                                                  ngtcp2_tstamp ts);
-
-/**
- * @function
- *
- * `ngtcp2_conn_client_write_handshake` is just like
- * `ngtcp2_conn_write_handshake`, but it is for client only, and can
- * write 0-RTT data.  This function can coalesce handshake packet and
- * 0-RTT packet into single UDP packet, thus it is generally more
- * efficient than the combination of `ngtcp2_conn_write_handshake` and
- * `ngtcp2_conn_write_stream`.
- *
- * |stream_id|, |fin|, |datav|, and |datavcnt| are stream identifier
- * to which 0-RTT data is sent, whether it is a last data chunk in
- * this stream, a vector of 0-RTT data, and its number of elements
- * respectively.  If there is no 0RTT data to send, pass negative
- * integer to |stream_id|.  The amount of 0RTT data sent is assigned
- * to |*pdatalen|.  If no data is sent, -1 is assigned.  Note that 0
- * length STREAM frame is allowed in QUIC, so 0 might be assigned to
- * |*pdatalen|.
- *
- * This function returns 0 if it cannot write any frame because buffer
- * is too small, or packet is congestion limited.  Application should
- * keep reading and wait for congestion window to grow.
- *
- * This function returns the number of bytes written to the buffer
- * pointed by |dest| if it succeeds, or one of the following negative
- * error codes: (TBD).
- */
-NGTCP2_EXTERN ssize_t ngtcp2_conn_client_write_handshake(
-    ngtcp2_conn *conn, uint8_t *dest, size_t destlen, ssize_t *pdatalen,
-    int64_t stream_id, uint8_t fin, const ngtcp2_vec *datav, size_t datavcnt,
-    ngtcp2_tstamp ts);
-
 /**
  * @function
  *
  * `ngtcp2_conn_read_pkt` decrypts QUIC packet given in |pkt| of
  * length |pktlen| and processes it.  |path| is the network path the
- * packet is delivered.  This function must be called after QUIC
- * handshake has finished successfully.
+ * packet is delivered.  This function performs QUIC handshake as
+ * well.
  *
  * This function must not be called from inside the callback
  * functions.
@@ -1709,45 +1613,9 @@ NGTCP2_EXTERN int ngtcp2_conn_read_pkt(ngtcp2_conn *conn,
 /**
  * @function
  *
- * `ngtcp2_conn_write_pkt` writes a QUIC packet in the buffer pointed
- * by |dest| whose length is |destlen|.  |ts| is the timestamp of the
- * current time.
- *
- * If |path| is not NULL, this function stores the network path with
- * which the packet should be sent.  Each addr field must point to the
- * buffer which is at least 128 bytes.  ``sizeof(struct
- * sockaddr_storage)`` is enough.  The assignment might not be done if
- * nothing is written to |dest|.
- *
- * If there is no packet to send, this function returns 0.
- *
- * Application should keep calling this function repeatedly until it
- * returns zero, or negative error code.
- *
- * This function returns 0 if it cannot write any frame because buffer
- * is too small, or packet is congestion limited.  Application should
- * keep reading and wait for congestion window to grow.
- *
- * This function must not be called from inside the callback
- * functions.
- *
- * This function returns the number of bytes written in |dest| if it
- * succeeds, or one of the following negative error codes:
- *
- * :enum:`NGTCP2_ERR_NOMEM`
- *     Out of memory.
- * :enum:`NGTCP2_ERR_CALLBACK_FAILURE`
- *     User-defined callback function failed.
- * :enum:`NGTCP2_ERR_PKT_NUM_EXHAUSTED`
- *     The packet number has reached at the maximum value, therefore
- *     the function cannot make new packet on this connection.
- *
- * In general, if the error code which satisfies
- * ngtcp2_erro_is_fatal(err) != 0 is returned, the application should
- * just close the connection by calling
- * `ngtcp2_conn_write_connection_close` or just delete the QUIC
- * connection using `ngtcp2_conn_del`.  It is undefined to call the
- * other library functions.
+ * `ngtcp2_conn_write_pkt` is equivalent to calling
+ * `ngtcp2_conn_writev_stream` without specifying stream data and
+ * :enum:`NGTCP2_WRITE_STREAM_FLAG_NONE` as flags.
  */
 NGTCP2_EXTERN ssize_t ngtcp2_conn_write_pkt(ngtcp2_conn *conn,
                                             ngtcp2_path *path, uint8_t *dest,
@@ -2018,9 +1886,8 @@ NGTCP2_EXTERN int ngtcp2_conn_initiate_key_update(ngtcp2_conn *conn);
  * `ngtcp2_conn_loss_detection_expiry` returns the expiry time point
  * of loss detection timer.  Application should call
  * `ngtcp2_conn_on_loss_detection_timer` and `ngtcp2_conn_write_pkt`
- * (or `ngtcp2_conn_write_handshake` if handshake has not finished
- * yet) when it expires.  It returns UINT64_MAX if loss detection
- * timer is not armed.
+ * (or `ngtcp2_conn_writev_stream`) when it expires.  It returns
+ * UINT64_MAX if loss detection timer is not armed.
  */
 NGTCP2_EXTERN ngtcp2_tstamp
 ngtcp2_conn_loss_detection_expiry(ngtcp2_conn *conn);
@@ -2031,9 +1898,8 @@ ngtcp2_conn_loss_detection_expiry(ngtcp2_conn *conn);
  * `ngtcp2_conn_ack_delay_expiry` returns the expiry time point of
  * delayed protected ACK.  Application should call
  * ngtcp2_conn_cancel_expired_ack_delay_timer() and
- * `ngtcp2_conn_write_pkt` (or `ngtcp2_conn_write_handshake` if
- * handshake has not finished yet) when it expires.  It returns
- * UINT64_MAX if there is no expiry.
+ * `ngtcp2_conn_write_pkt` (or `ngtcp2_conn_writev_stream`) when it
+ * expires.  It returns UINT64_MAX if there is no expiry.
  */
 NGTCP2_EXTERN ngtcp2_tstamp ngtcp2_conn_ack_delay_expiry(ngtcp2_conn *conn);
 
@@ -2225,6 +2091,21 @@ NGTCP2_EXTERN int ngtcp2_conn_shutdown_stream_read(ngtcp2_conn *conn,
                                                    int64_t stream_id,
                                                    uint16_t app_error_code);
 
+/**
+ * @enum
+ *
+ * ngtcp2_write_stream_flag defines extra behaviour for
+ * `ngtcp2_conn_writev_stream()`.
+ */
+typedef enum {
+  NGTCP2_WRITE_STREAM_FLAG_NONE = 0x00,
+  /**
+   * NGTCP2_WRITE_STREAM_FLAG_MORE indicates that more stream data may
+   * come and should be coalesced into the same packet if possible.
+   */
+  NGTCP2_WRITE_STREAM_FLAG_MORE = 0x01
+} ngtcp2_write_stream_flag;
+
 /**
  * @function
  *
@@ -2234,15 +2115,18 @@ NGTCP2_EXTERN int ngtcp2_conn_shutdown_stream_read(ngtcp2_conn *conn,
  */
 NGTCP2_EXTERN ssize_t ngtcp2_conn_write_stream(
     ngtcp2_conn *conn, ngtcp2_path *path, uint8_t *dest, size_t destlen,
-    ssize_t *pdatalen, int64_t stream_id, uint8_t fin, const uint8_t *data,
-    size_t datalen, ngtcp2_tstamp ts);
+    ssize_t *pdatalen, uint32_t flags, int64_t stream_id, uint8_t fin,
+    const uint8_t *data, size_t datalen, ngtcp2_tstamp ts);
 
 /**
  * @function
  *
  * `ngtcp2_conn_writev_stream` writes a packet containing stream data
  * of stream denoted by |stream_id|.  The buffer of the packet is
- * pointed by |dest| of length |destlen|.
+ * pointed by |dest| of length |destlen|.  This function performs QUIC
+ * handshake as well.
+ *
+ * Specifying -1 to |stream_id| means no new stream data to send.
  *
  * If |path| is not NULL, this function stores the network path with
  * which the packet should be sent.  Each addr field must point to the
@@ -2250,7 +2134,7 @@ NGTCP2_EXTERN ssize_t ngtcp2_conn_write_stream(
  * sockaddr_storage)`` is enough.  The assignment might not be done if
  * nothing is written to |dest|.
  *
- * If the all given data is encoded as STREAM frame in|dest|, and if
+ * If the all given data is encoded as STREAM frame in |dest|, and if
  * |fin| is nonzero, fin flag is set in outgoing STREAM frame.
  * Otherwise, fin flag in STREAM frame is not set.
  *
@@ -2264,6 +2148,44 @@ NGTCP2_EXTERN ssize_t ngtcp2_conn_write_stream(
  * The number of data encoded in STREAM frame is stored in |*pdatalen|
  * if it is not NULL.
  *
+ * If |flags| equals to :enum:`NGTCP2_WRITE_STREAM_FLAG_NONE`, this
+ * function produces a single payload of UDP packet.  If the given
+ * stream data is small (e.g., few bytes), the packet might be
+ * severely under filled.  Too many small packet might increase
+ * overall packet processing costs.  Unless there are retransmissions,
+ * by default, application can only send 1 STREAM frame in one QUIC
+ * packet.  In order to include more than 1 STREAM frame in one QUIC
+ * packet, specify :enum:`NGTCP2_WRITE_STREAM_FLAG_MORE` in |flags|.
+ * This is analogous to ``MSG_MORE`` flag in ``send(2)``.  If the
+ * :enum:`NGTCP2_WRITE_STREAM_FLAG_MORE` is used, there are 4
+ * outcomes:
+ *
+ * - The function returns the written length of packet just like
+ *   without :enum:`NGTCP2_WRITE_STREAM_FLAG_MORE`.  This is because
+ *   packet is nearly full and the library decided to make a complete
+ *   packet.
+ *
+ * - The function returns :enum:`NGTCP2_ERR_WRITE_STREAM_MORE`.  This
+ *   indicates that application can call this function with different
+ *   stream data to pack them into the same packet.  Application has
+ *   to specify the same |conn|, |path|, |dest|, |destlen|,
+ *   |pdatalen|, and |ts| parameters, otherwise the behaviour is
+ *   undefined.  The application can change |flags|.
+ *
+ * - The function returns :enum:`NGTCP2_ERR_STREAM_DATA_BLOCKED` which
+ *   indicates that stream is blocked because of flow control.
+ *
+ * - The other error might be returned just like without
+ *   :enum:`NGTCP2_WRITE_STREAM_FLAG_MORE`.
+ *
+ * When application sees :enum:`NGTCP2_ERR_WRITE_STREAM_MORE`, it must
+ * not call other ngtcp2 API functions (application can still call
+ * `ngtcp2_conn_write_connection_close` or
+ * `ngtcp2_conn_write_application_close` to handle error from this
+ * function).  Just keep calling `ngtcp2_conn_writev_stream` or
+ * `ngtcp2_conn_write_pkt` until it returns a positive number (which
+ * indicates a complete packet is ready).
+ *
  * This function returns 0 if it cannot write any frame because buffer
  * is too small, or packet is congestion limited.  Application should
  * keep reading and wait for congestion window to grow.
@@ -2290,11 +2212,22 @@ NGTCP2_EXTERN ssize_t ngtcp2_conn_write_stream(
  *     Early data was rejected by server.
  * :enum:`NGTCP2_ERR_STREAM_DATA_BLOCKED`
  *     Stream is blocked because of flow control.
+ * :enum:`NGTCP2_ERR_WRITE_STREAM_MORE`
+ *     (Only when :enum:`NGTCP2_WRITE_STREAM_FLAG_MORE` is specified)
+ *     Application can call this function to pack more stream data
+ *     into the same packet.  See above to know how it works.
+ *
+ * In general, if the error code which satisfies
+ * ngtcp2_err_is_fatal(err) != 0 is returned, the application should
+ * just close the connection by calling
+ * `ngtcp2_conn_write_connection_close` or just delete the QUIC
+ * connection using `ngtcp2_conn_del`.  It is undefined to call the
+ * other library functions.
  */
 NGTCP2_EXTERN ssize_t ngtcp2_conn_writev_stream(
     ngtcp2_conn *conn, ngtcp2_path *path, uint8_t *dest, size_t destlen,
-    ssize_t *pdatalen, int64_t stream_id, uint8_t fin, const ngtcp2_vec *datav,
-    size_t datavcnt, ngtcp2_tstamp ts);
+    ssize_t *pdatalen, uint32_t flags, int64_t stream_id, uint8_t fin,
+    const ngtcp2_vec *datav, size_t datavcnt, ngtcp2_tstamp ts);
 
 /**
  * @function
diff --git a/lib/ngtcp2_conn.c b/lib/ngtcp2_conn.c
index 605554ca49792805daa77a89e82c78502b4ca798..23fe906b31e5aa5e465f1ef056758de9e88aaf01 100644
--- a/lib/ngtcp2_conn.c
+++ b/lib/ngtcp2_conn.c
@@ -28,7 +28,6 @@
 #include <assert.h>
 #include <math.h>
 
-#include "ngtcp2_ppe.h"
 #include "ngtcp2_macro.h"
 #include "ngtcp2_log.h"
 #include "ngtcp2_cid.h"
@@ -2096,6 +2095,16 @@ static int conn_remove_retired_connection_id(ngtcp2_conn *conn,
   return 0;
 }
 
+typedef enum {
+  NGTCP2_WRITE_PKT_FLAG_NONE = 0x00,
+  /* NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING indicates that packet
+     should be padded */
+  NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING = 0x01,
+  /* NGTCP2_WRITE_PKT_FLAG_STREAM_MORE indicates that more stream DATA
+     may come and it should be encoded into the current packet. */
+  NGTCP2_WRITE_PKT_FLAG_STREAM_MORE = 0x02,
+} ngtcp2_write_pkt_flag;
+
 /*
  * conn_write_pkt writes a protected packet in the buffer pointed by
  * |dest| whose length if |destlen|.  |type| specifies the type of
@@ -2127,13 +2136,13 @@ static ssize_t conn_write_pkt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen,
                               ssize_t *pdatalen, uint8_t type,
                               ngtcp2_strm *data_strm, uint8_t fin,
                               const ngtcp2_vec *datav, size_t datavcnt,
-                              int require_padding, ngtcp2_tstamp ts) {
+                              uint8_t flags, ngtcp2_tstamp ts) {
   int rv;
-  ngtcp2_ppe ppe;
-  ngtcp2_pkt_hd hd;
+  ngtcp2_crypto_ctx *ctx = &conn->pkt.ctx;
+  ngtcp2_ppe *ppe = &conn->pkt.ppe;
+  ngtcp2_pkt_hd *hd = &conn->pkt.hd;
   ngtcp2_frame *ackfr = NULL, lfr;
   ssize_t nwrite;
-  ngtcp2_crypto_ctx ctx;
   ngtcp2_frame_chain **pfrc, *nfrc, *frc;
   ngtcp2_stream_frame_chain *nsfrc;
   ngtcp2_crypto_frame_chain *ncfrc;
@@ -2150,34 +2159,9 @@ static ssize_t conn_write_pkt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen,
   int hd_logged = 0;
   ngtcp2_path_challenge_entry *pcent;
   uint8_t hd_flags;
-
-  switch (type) {
-  case NGTCP2_PKT_SHORT:
-    hd_flags =
-        (pktns->crypto.tx.ckm->flags & NGTCP2_CRYPTO_KM_FLAG_KEY_PHASE_ONE)
-            ? NGTCP2_PKT_FLAG_KEY_PHASE
-            : NGTCP2_PKT_FLAG_NONE;
-    ctx.ckm = pktns->crypto.tx.ckm;
-    ctx.hp = pktns->crypto.tx.hp;
-    break;
-  case NGTCP2_PKT_0RTT:
-    assert(!conn->server);
-    if (!conn->early.ckm) {
-      return 0;
-    }
-    hd_flags = NGTCP2_PKT_FLAG_LONG_FORM;
-    ctx.ckm = conn->early.ckm;
-    ctx.hp = conn->early.hp;
-    break;
-  default:
-    /* Unreachable */
-    assert(0);
-  }
-
-  ctx.aead_overhead = conn->crypto.aead_overhead;
-  ctx.encrypt = conn->callbacks.encrypt;
-  ctx.hp_mask = conn->callbacks.hp_mask;
-  ctx.user_data = conn;
+  int require_padding = (flags & NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING) != 0;
+  int stream_more = (flags & NGTCP2_WRITE_PKT_FLAG_STREAM_MORE) != 0;
+  int ppe_pending = (conn->flags & NGTCP2_CONN_FLAG_PPE_PENDING) != 0;
 
   if (data_strm) {
     ndatalen = conn_enforce_flow_control(conn, data_strm, datalen);
@@ -2196,221 +2180,225 @@ static ssize_t conn_write_pkt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen,
     }
   }
 
-  /* TODO Take into account stream frames */
-  if ((pktns->tx.frq || send_stream ||
-       ngtcp2_ringbuf_len(&conn->rx.path_challenge) ||
-       conn_should_send_max_data(conn)) &&
-      conn->rx.unsent_max_offset > conn->rx.max_offset) {
-    rv = ngtcp2_frame_chain_new(&nfrc, conn->mem);
-    if (rv != 0) {
-      return rv;
-    }
-    nfrc->fr.type = NGTCP2_FRAME_MAX_DATA;
-    nfrc->fr.max_data.max_data = conn->rx.unsent_max_offset;
-    nfrc->next = pktns->tx.frq;
-    pktns->tx.frq = nfrc;
-
-    conn->rx.max_offset = conn->rx.unsent_max_offset;
-  }
-
-  ngtcp2_pkt_hd_init(&hd, hd_flags, type, &conn->dcid.current.cid, &conn->oscid,
-                     pktns->tx.last_pkt_num + 1, pktns_select_pkt_numlen(pktns),
-                     conn->version, 0);
-
-  ngtcp2_ppe_init(&ppe, dest, destlen, &ctx);
-
-  rv = ngtcp2_ppe_encode_hd(&ppe, &hd);
-  if (rv != 0) {
-    assert(NGTCP2_ERR_NOBUF == rv);
-    return 0;
-  }
-
-  if (!ngtcp2_ppe_ensure_hp_sample(&ppe)) {
-    return 0;
-  }
-
-  for (; ngtcp2_ringbuf_len(&conn->rx.path_challenge);) {
-    pcent = ngtcp2_ringbuf_get(&conn->rx.path_challenge, 0);
-
-    /* PATH_RESPONSE is bound to the path that the corresponding
-       PATH_CHALLENGE is received. */
-    if (!ngtcp2_path_eq(&conn->dcid.current.ps.path, &pcent->ps.path)) {
+  if (!ppe_pending) {
+    switch (type) {
+    case NGTCP2_PKT_SHORT:
+      hd_flags =
+          (pktns->crypto.tx.ckm->flags & NGTCP2_CRYPTO_KM_FLAG_KEY_PHASE_ONE)
+              ? NGTCP2_PKT_FLAG_KEY_PHASE
+              : NGTCP2_PKT_FLAG_NONE;
+      ctx->ckm = pktns->crypto.tx.ckm;
+      ctx->hp = pktns->crypto.tx.hp;
       break;
-    }
-
-    lfr.type = NGTCP2_FRAME_PATH_RESPONSE;
-    memcpy(lfr.path_response.data, pcent->data, sizeof(lfr.path_response.data));
-
-    rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, &lfr);
-    if (rv != 0) {
-      assert(NGTCP2_ERR_NOBUF == rv);
+    case NGTCP2_PKT_0RTT:
+      assert(!conn->server);
+      if (!conn->early.ckm) {
+        return 0;
+      }
+      hd_flags = NGTCP2_PKT_FLAG_LONG_FORM;
+      ctx->ckm = conn->early.ckm;
+      ctx->hp = conn->early.hp;
       break;
+    default:
+      /* Unreachable */
+      assert(0);
     }
 
-    ngtcp2_ringbuf_pop_front(&conn->rx.path_challenge);
-
-    pkt_empty = 0;
-    rtb_entry_flags |= NGTCP2_RTB_FLAG_ACK_ELICITING;
-    /* We don't retransmit PATH_RESPONSE. */
-  }
+    ctx->aead_overhead = conn->crypto.aead_overhead;
+    ctx->encrypt = conn->callbacks.encrypt;
+    ctx->hp_mask = conn->callbacks.hp_mask;
+    ctx->user_data = conn;
 
-  rv = conn_create_ack_frame(conn, &ackfr, &pktns->acktr, ts,
-                             conn_compute_ack_delay(conn),
-                             conn->local.settings.ack_delay_exponent);
-  if (rv != 0) {
-    assert(ngtcp2_err_is_fatal(rv));
-    return rv;
-  }
+    /* TODO Take into account stream frames */
+    if ((pktns->tx.frq || send_stream ||
+         ngtcp2_ringbuf_len(&conn->rx.path_challenge) ||
+         conn_should_send_max_data(conn)) &&
+        conn->rx.unsent_max_offset > conn->rx.max_offset) {
+      rv = ngtcp2_frame_chain_new(&nfrc, conn->mem);
+      if (rv != 0) {
+        return rv;
+      }
+      nfrc->fr.type = NGTCP2_FRAME_MAX_DATA;
+      nfrc->fr.max_data.max_data = conn->rx.unsent_max_offset;
+      nfrc->next = pktns->tx.frq;
+      pktns->tx.frq = nfrc;
 
-  if (ackfr) {
-    rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, ackfr);
-    if (rv != 0) {
-      assert(NGTCP2_ERR_NOBUF == rv);
-    } else {
-      ngtcp2_acktr_commit_ack(&pktns->acktr);
-      ngtcp2_acktr_add_ack(&pktns->acktr, hd.pkt_num, ackfr->ack.largest_ack);
-      pkt_empty = 0;
+      conn->rx.max_offset = conn->rx.unsent_max_offset;
     }
-    ngtcp2_mem_free(conn->mem, ackfr);
-    ackfr = NULL;
-  }
 
-  for (pfrc = &pktns->tx.frq; *pfrc;) {
-    switch ((*pfrc)->fr.type) {
-    case NGTCP2_FRAME_STOP_SENDING:
-      strm = ngtcp2_conn_find_stream(conn, (*pfrc)->fr.stop_sending.stream_id);
-      if (strm == NULL || (strm->flags & NGTCP2_STRM_FLAG_SHUT_RD)) {
-        frc = *pfrc;
-        *pfrc = (*pfrc)->next;
-        ngtcp2_frame_chain_del(frc, conn->mem);
-        continue;
-      }
-      break;
-    case NGTCP2_FRAME_STREAM:
-      assert(0);
-      break;
-    case NGTCP2_FRAME_MAX_STREAMS_BIDI:
-      if ((*pfrc)->fr.max_streams.max_streams < conn->remote.bidi.max_streams) {
-        frc = *pfrc;
-        *pfrc = (*pfrc)->next;
-        ngtcp2_frame_chain_del(frc, conn->mem);
-        continue;
-      }
-      break;
-    case NGTCP2_FRAME_MAX_STREAMS_UNI:
-      if ((*pfrc)->fr.max_streams.max_streams < conn->remote.uni.max_streams) {
-        frc = *pfrc;
-        *pfrc = (*pfrc)->next;
-        ngtcp2_frame_chain_del(frc, conn->mem);
-        continue;
-      }
-      break;
-    case NGTCP2_FRAME_MAX_STREAM_DATA:
-      strm =
-          ngtcp2_conn_find_stream(conn, (*pfrc)->fr.max_stream_data.stream_id);
-      if (strm == NULL || (strm->flags & NGTCP2_STRM_FLAG_SHUT_RD) ||
-          (*pfrc)->fr.max_stream_data.max_stream_data < strm->rx.max_offset) {
-        frc = *pfrc;
-        *pfrc = (*pfrc)->next;
-        ngtcp2_frame_chain_del(frc, conn->mem);
-        continue;
-      }
-      break;
-    case NGTCP2_FRAME_MAX_DATA:
-      if ((*pfrc)->fr.max_data.max_data < conn->rx.max_offset) {
-        frc = *pfrc;
-        *pfrc = (*pfrc)->next;
-        ngtcp2_frame_chain_del(frc, conn->mem);
-        continue;
-      }
-      break;
-    case NGTCP2_FRAME_CRYPTO:
-      assert(0);
-      break;
-    }
+    ngtcp2_pkt_hd_init(hd, hd_flags, type, &conn->dcid.current.cid,
+                       &conn->oscid, pktns->tx.last_pkt_num + 1,
+                       pktns_select_pkt_numlen(pktns), conn->version, 0);
 
-    rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, &(*pfrc)->fr);
+    ngtcp2_ppe_init(ppe, dest, destlen, ctx);
+
+    rv = ngtcp2_ppe_encode_hd(ppe, hd);
     if (rv != 0) {
       assert(NGTCP2_ERR_NOBUF == rv);
-      break;
+      return 0;
     }
 
-    pkt_empty = 0;
-    rtb_entry_flags |= NGTCP2_RTB_FLAG_ACK_ELICITING;
-    pfrc = &(*pfrc)->next;
-  }
-
-  if (rv != NGTCP2_ERR_NOBUF) {
-    for (; !ngtcp2_pq_empty(&pktns->crypto.tx.frq);) {
-      left = ngtcp2_ppe_left(&ppe);
+    if (!ngtcp2_ppe_ensure_hp_sample(ppe)) {
+      return 0;
+    }
 
-      left = ngtcp2_pkt_crypto_max_datalen(
-          conn_cryptofrq_top(conn, pktns)->fr.offset, left, left);
+    for (; ngtcp2_ringbuf_len(&conn->rx.path_challenge);) {
+      pcent = ngtcp2_ringbuf_get(&conn->rx.path_challenge, 0);
 
-      if (left == (size_t)-1) {
+      /* PATH_RESPONSE is bound to the path that the corresponding
+         PATH_CHALLENGE is received. */
+      if (!ngtcp2_path_eq(&conn->dcid.current.ps.path, &pcent->ps.path)) {
         break;
       }
 
-      rv = conn_cryptofrq_pop(conn, &ncfrc, pktns, left);
-      if (rv != 0) {
-        assert(ngtcp2_err_is_fatal(rv));
-        return rv;
-      }
+      lfr.type = NGTCP2_FRAME_PATH_RESPONSE;
+      memcpy(lfr.path_response.data, pcent->data,
+             sizeof(lfr.path_response.data));
 
-      if (ncfrc == NULL) {
-        break;
-      }
-
-      rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd,
-                                       &ncfrc->frc.fr);
+      rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, &lfr);
       if (rv != 0) {
-        assert(0);
+        assert(NGTCP2_ERR_NOBUF == rv);
+        break;
       }
 
-      *pfrc = &ncfrc->frc;
-      pfrc = &(*pfrc)->next;
+      ngtcp2_ringbuf_pop_front(&conn->rx.path_challenge);
 
       pkt_empty = 0;
       rtb_entry_flags |= NGTCP2_RTB_FLAG_ACK_ELICITING;
+      /* We don't retransmit PATH_RESPONSE. */
     }
-  }
 
-  /* Write MAX_STREAM_ID after RESET_STREAM so that we can extend stream
-     ID space in one packet. */
-  if (rv != NGTCP2_ERR_NOBUF && *pfrc == NULL &&
-      conn->remote.bidi.unsent_max_streams > conn->remote.bidi.max_streams) {
-    rv = conn_call_extend_max_remote_streams_bidi(
-        conn, conn->remote.bidi.unsent_max_streams);
+    rv = conn_create_ack_frame(conn, &ackfr, &pktns->acktr, ts,
+                               conn_compute_ack_delay(conn),
+                               conn->local.settings.ack_delay_exponent);
     if (rv != 0) {
       assert(ngtcp2_err_is_fatal(rv));
       return rv;
     }
 
-    rv = ngtcp2_frame_chain_new(&nfrc, conn->mem);
-    if (rv != 0) {
-      assert(ngtcp2_err_is_fatal(rv));
-      return rv;
+    if (ackfr) {
+      rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, ackfr);
+      if (rv != 0) {
+        assert(NGTCP2_ERR_NOBUF == rv);
+      } else {
+        ngtcp2_acktr_commit_ack(&pktns->acktr);
+        ngtcp2_acktr_add_ack(&pktns->acktr, hd->pkt_num,
+                             ackfr->ack.largest_ack);
+        pkt_empty = 0;
+      }
+      ngtcp2_mem_free(conn->mem, ackfr);
+      ackfr = NULL;
     }
-    nfrc->fr.type = NGTCP2_FRAME_MAX_STREAMS_BIDI;
-    nfrc->fr.max_streams.max_streams = conn->remote.bidi.unsent_max_streams;
-    *pfrc = nfrc;
 
-    conn->remote.bidi.max_streams = conn->remote.bidi.unsent_max_streams;
+    for (pfrc = &pktns->tx.frq; *pfrc;) {
+      switch ((*pfrc)->fr.type) {
+      case NGTCP2_FRAME_STOP_SENDING:
+        strm =
+            ngtcp2_conn_find_stream(conn, (*pfrc)->fr.stop_sending.stream_id);
+        if (strm == NULL || (strm->flags & NGTCP2_STRM_FLAG_SHUT_RD)) {
+          frc = *pfrc;
+          *pfrc = (*pfrc)->next;
+          ngtcp2_frame_chain_del(frc, conn->mem);
+          continue;
+        }
+        break;
+      case NGTCP2_FRAME_STREAM:
+        assert(0);
+        break;
+      case NGTCP2_FRAME_MAX_STREAMS_BIDI:
+        if ((*pfrc)->fr.max_streams.max_streams <
+            conn->remote.bidi.max_streams) {
+          frc = *pfrc;
+          *pfrc = (*pfrc)->next;
+          ngtcp2_frame_chain_del(frc, conn->mem);
+          continue;
+        }
+        break;
+      case NGTCP2_FRAME_MAX_STREAMS_UNI:
+        if ((*pfrc)->fr.max_streams.max_streams <
+            conn->remote.uni.max_streams) {
+          frc = *pfrc;
+          *pfrc = (*pfrc)->next;
+          ngtcp2_frame_chain_del(frc, conn->mem);
+          continue;
+        }
+        break;
+      case NGTCP2_FRAME_MAX_STREAM_DATA:
+        strm = ngtcp2_conn_find_stream(conn,
+                                       (*pfrc)->fr.max_stream_data.stream_id);
+        if (strm == NULL || (strm->flags & NGTCP2_STRM_FLAG_SHUT_RD) ||
+            (*pfrc)->fr.max_stream_data.max_stream_data < strm->rx.max_offset) {
+          frc = *pfrc;
+          *pfrc = (*pfrc)->next;
+          ngtcp2_frame_chain_del(frc, conn->mem);
+          continue;
+        }
+        break;
+      case NGTCP2_FRAME_MAX_DATA:
+        if ((*pfrc)->fr.max_data.max_data < conn->rx.max_offset) {
+          frc = *pfrc;
+          *pfrc = (*pfrc)->next;
+          ngtcp2_frame_chain_del(frc, conn->mem);
+          continue;
+        }
+        break;
+      case NGTCP2_FRAME_CRYPTO:
+        assert(0);
+        break;
+      }
+
+      rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, &(*pfrc)->fr);
+      if (rv != 0) {
+        assert(NGTCP2_ERR_NOBUF == rv);
+        break;
+      }
 
-    rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, &(*pfrc)->fr);
-    if (rv != 0) {
-      assert(NGTCP2_ERR_NOBUF == rv);
-    } else {
       pkt_empty = 0;
       rtb_entry_flags |= NGTCP2_RTB_FLAG_ACK_ELICITING;
       pfrc = &(*pfrc)->next;
     }
-  }
 
-  if (rv != NGTCP2_ERR_NOBUF && *pfrc == NULL) {
-    if (conn->remote.uni.unsent_max_streams > conn->remote.uni.max_streams) {
-      rv = conn_call_extend_max_remote_streams_uni(
-          conn, conn->remote.uni.unsent_max_streams);
+    if (rv != NGTCP2_ERR_NOBUF) {
+      for (; !ngtcp2_pq_empty(&pktns->crypto.tx.frq);) {
+        left = ngtcp2_ppe_left(ppe);
+
+        left = ngtcp2_pkt_crypto_max_datalen(
+            conn_cryptofrq_top(conn, pktns)->fr.offset, left, left);
+
+        if (left == (size_t)-1) {
+          break;
+        }
+
+        rv = conn_cryptofrq_pop(conn, &ncfrc, pktns, left);
+        if (rv != 0) {
+          assert(ngtcp2_err_is_fatal(rv));
+          return rv;
+        }
+
+        if (ncfrc == NULL) {
+          break;
+        }
+
+        rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd,
+                                         &ncfrc->frc.fr);
+        if (rv != 0) {
+          assert(0);
+        }
+
+        *pfrc = &ncfrc->frc;
+        pfrc = &(*pfrc)->next;
+
+        pkt_empty = 0;
+        rtb_entry_flags |= NGTCP2_RTB_FLAG_ACK_ELICITING;
+      }
+    }
+
+    /* Write MAX_STREAM_ID after RESET_STREAM so that we can extend stream
+       ID space in one packet. */
+    if (rv != NGTCP2_ERR_NOBUF && *pfrc == NULL &&
+        conn->remote.bidi.unsent_max_streams > conn->remote.bidi.max_streams) {
+      rv = conn_call_extend_max_remote_streams_bidi(
+          conn, conn->remote.bidi.unsent_max_streams);
       if (rv != 0) {
         assert(ngtcp2_err_is_fatal(rv));
         return rv;
@@ -2421,14 +2409,13 @@ static ssize_t conn_write_pkt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen,
         assert(ngtcp2_err_is_fatal(rv));
         return rv;
       }
-      nfrc->fr.type = NGTCP2_FRAME_MAX_STREAMS_UNI;
-      nfrc->fr.max_streams.max_streams = conn->remote.uni.unsent_max_streams;
+      nfrc->fr.type = NGTCP2_FRAME_MAX_STREAMS_BIDI;
+      nfrc->fr.max_streams.max_streams = conn->remote.bidi.unsent_max_streams;
       *pfrc = nfrc;
 
-      conn->remote.uni.max_streams = conn->remote.uni.unsent_max_streams;
+      conn->remote.bidi.max_streams = conn->remote.bidi.unsent_max_streams;
 
-      rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd,
-                                       &(*pfrc)->fr);
+      rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, &(*pfrc)->fr);
       if (rv != 0) {
         assert(NGTCP2_ERR_NOBUF == rv);
       } else {
@@ -2437,91 +2424,127 @@ static ssize_t conn_write_pkt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen,
         pfrc = &(*pfrc)->next;
       }
     }
-  }
 
-  if (rv != NGTCP2_ERR_NOBUF) {
-    for (; !ngtcp2_pq_empty(&conn->tx.strmq);) {
-      strm = ngtcp2_conn_tx_strmq_top(conn);
+    if (rv != NGTCP2_ERR_NOBUF && *pfrc == NULL) {
+      if (conn->remote.uni.unsent_max_streams > conn->remote.uni.max_streams) {
+        rv = conn_call_extend_max_remote_streams_uni(
+            conn, conn->remote.uni.unsent_max_streams);
+        if (rv != 0) {
+          assert(ngtcp2_err_is_fatal(rv));
+          return rv;
+        }
 
-      if (!(strm->flags & NGTCP2_STRM_FLAG_SHUT_RD) &&
-          strm->rx.max_offset < strm->rx.unsent_max_offset) {
         rv = ngtcp2_frame_chain_new(&nfrc, conn->mem);
         if (rv != 0) {
           assert(ngtcp2_err_is_fatal(rv));
           return rv;
         }
-        nfrc->fr.type = NGTCP2_FRAME_MAX_STREAM_DATA;
-        nfrc->fr.max_stream_data.stream_id = strm->stream_id;
-        nfrc->fr.max_stream_data.max_stream_data = strm->rx.unsent_max_offset;
-        ngtcp2_list_insert(nfrc, pfrc);
+        nfrc->fr.type = NGTCP2_FRAME_MAX_STREAMS_UNI;
+        nfrc->fr.max_streams.max_streams = conn->remote.uni.unsent_max_streams;
+        *pfrc = nfrc;
 
-        rv =
-            conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd, &nfrc->fr);
+        conn->remote.uni.max_streams = conn->remote.uni.unsent_max_streams;
+
+        rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd,
+                                         &(*pfrc)->fr);
         if (rv != 0) {
           assert(NGTCP2_ERR_NOBUF == rv);
-          break;
+        } else {
+          pkt_empty = 0;
+          rtb_entry_flags |= NGTCP2_RTB_FLAG_ACK_ELICITING;
+          pfrc = &(*pfrc)->next;
         }
-
-        pkt_empty = 0;
-        rtb_entry_flags |= NGTCP2_RTB_FLAG_ACK_ELICITING;
-        pfrc = &(*pfrc)->next;
-        strm->rx.max_offset = strm->rx.unsent_max_offset;
       }
+    }
 
-      if (ngtcp2_strm_streamfrq_empty(strm)) {
-        ngtcp2_conn_tx_strmq_pop(conn);
-        continue;
-      }
+    if (rv != NGTCP2_ERR_NOBUF) {
+      for (; !ngtcp2_pq_empty(&conn->tx.strmq);) {
+        strm = ngtcp2_conn_tx_strmq_top(conn);
 
-      left = ngtcp2_ppe_left(&ppe);
+        if (!(strm->flags & NGTCP2_STRM_FLAG_SHUT_RD) &&
+            strm->rx.max_offset < strm->rx.unsent_max_offset) {
+          rv = ngtcp2_frame_chain_new(&nfrc, conn->mem);
+          if (rv != 0) {
+            assert(ngtcp2_err_is_fatal(rv));
+            return rv;
+          }
+          nfrc->fr.type = NGTCP2_FRAME_MAX_STREAM_DATA;
+          nfrc->fr.max_stream_data.stream_id = strm->stream_id;
+          nfrc->fr.max_stream_data.max_stream_data = strm->rx.unsent_max_offset;
+          ngtcp2_list_insert(nfrc, pfrc);
 
-      left = ngtcp2_pkt_stream_max_datalen(
-          strm->stream_id, ngtcp2_strm_streamfrq_top(strm)->fr.offset, left,
-          left);
+          rv =
+              conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, &nfrc->fr);
+          if (rv != 0) {
+            assert(NGTCP2_ERR_NOBUF == rv);
+            break;
+          }
 
-      if (left == (size_t)-1) {
-        break;
-      }
+          pkt_empty = 0;
+          rtb_entry_flags |= NGTCP2_RTB_FLAG_ACK_ELICITING;
+          pfrc = &(*pfrc)->next;
+          strm->rx.max_offset = strm->rx.unsent_max_offset;
+        }
 
-      rv = ngtcp2_strm_streamfrq_pop(strm, &nsfrc, left);
-      if (rv != 0) {
-        assert(ngtcp2_err_is_fatal(rv));
-        return rv;
-      }
+        if (ngtcp2_strm_streamfrq_empty(strm)) {
+          ngtcp2_conn_tx_strmq_pop(conn);
+          continue;
+        }
 
-      if (nsfrc == NULL) {
-        /* TODO Why? */
-        break;
-      }
+        left = ngtcp2_ppe_left(ppe);
 
-      rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd,
-                                       &nsfrc->frc.fr);
-      if (rv != 0) {
-        assert(0);
-      }
+        left = ngtcp2_pkt_stream_max_datalen(
+            strm->stream_id, ngtcp2_strm_streamfrq_top(strm)->fr.offset, left,
+            left);
 
-      *pfrc = &nsfrc->frc;
-      pfrc = &(*pfrc)->next;
+        if (left == (size_t)-1) {
+          break;
+        }
 
-      pkt_empty = 0;
-      rtb_entry_flags |= NGTCP2_RTB_FLAG_ACK_ELICITING;
+        rv = ngtcp2_strm_streamfrq_pop(strm, &nsfrc, left);
+        if (rv != 0) {
+          assert(ngtcp2_err_is_fatal(rv));
+          return rv;
+        }
 
-      if (ngtcp2_strm_streamfrq_empty(strm)) {
-        ngtcp2_conn_tx_strmq_pop(conn);
-        continue;
-      }
+        if (nsfrc == NULL) {
+          /* TODO Why? */
+          break;
+        }
 
-      ngtcp2_conn_tx_strmq_pop(conn);
-      ++strm->cycle;
-      rv = ngtcp2_conn_tx_strmq_push(conn, strm);
-      if (rv != 0) {
-        assert(ngtcp2_err_is_fatal(rv));
-        return rv;
+        rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd,
+                                         &nsfrc->frc.fr);
+        if (rv != 0) {
+          assert(0);
+        }
+
+        *pfrc = &nsfrc->frc;
+        pfrc = &(*pfrc)->next;
+
+        pkt_empty = 0;
+        rtb_entry_flags |= NGTCP2_RTB_FLAG_ACK_ELICITING;
+
+        if (ngtcp2_strm_streamfrq_empty(strm)) {
+          ngtcp2_conn_tx_strmq_pop(conn);
+          continue;
+        }
+
+        ngtcp2_conn_tx_strmq_pop(conn);
+        ++strm->cycle;
+        rv = ngtcp2_conn_tx_strmq_push(conn, strm);
+        if (rv != 0) {
+          assert(ngtcp2_err_is_fatal(rv));
+          return rv;
+        }
       }
     }
+  } else {
+    pfrc = conn->pkt.pfrc;
+    rtb_entry_flags |= conn->pkt.rtb_entry_flags;
+    pkt_empty = conn->pkt.pkt_empty;
   }
 
-  left = ngtcp2_ppe_left(&ppe);
+  left = ngtcp2_ppe_left(ppe);
 
   if (rv != NGTCP2_ERR_NOBUF && send_stream && *pfrc == NULL &&
       (ndatalen = ngtcp2_pkt_stream_max_datalen(data_strm->stream_id,
@@ -2545,8 +2568,7 @@ static ssize_t conn_write_pkt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen,
     fin = fin && ndatalen == datalen;
     nsfrc->fr.fin = fin;
 
-    rv = conn_ppe_write_frame_hd_log(conn, &ppe, &hd_logged, &hd,
-                                     &nsfrc->frc.fr);
+    rv = conn_ppe_write_frame_hd_log(conn, ppe, &hd_logged, hd, &nsfrc->frc.fr);
     if (rv != 0) {
       assert(0);
     }
@@ -2556,6 +2578,17 @@ static ssize_t conn_write_pkt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen,
 
     pkt_empty = 0;
     rtb_entry_flags |= NGTCP2_RTB_FLAG_ACK_ELICITING;
+
+    data_strm->tx.offset += ndatalen;
+    conn->tx.offset += ndatalen;
+
+    if (fin) {
+      ngtcp2_strm_shutdown(data_strm, NGTCP2_STRM_FLAG_SHUT_WR);
+    }
+
+    if (pdatalen) {
+      *pdatalen = (ssize_t)ndatalen;
+    }
   } else {
     send_stream = 0;
   }
@@ -2568,30 +2601,44 @@ static ssize_t conn_write_pkt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen,
     return 0;
   }
 
+  if (stream_more) {
+    conn->pkt.pfrc = pfrc;
+    conn->pkt.pkt_empty = pkt_empty;
+    conn->pkt.rtb_entry_flags = rtb_entry_flags;
+    conn->flags |= NGTCP2_CONN_FLAG_PPE_PENDING;
+
+    if (stream_blocked) {
+      return NGTCP2_ERR_STREAM_DATA_BLOCKED;
+    }
+    if (send_stream) {
+      return NGTCP2_ERR_WRITE_STREAM_MORE;
+    }
+  }
+
   /* TODO Push STREAM frame back to ngtcp2_strm if there is an error
      before ngtcp2_rtb_entry is safely created and added. */
   if ((require_padding ||
        (type == NGTCP2_PKT_0RTT && conn->state == NGTCP2_CS_CLIENT_INITIAL)) &&
-      ngtcp2_ppe_left(&ppe)) {
+      ngtcp2_ppe_left(ppe)) {
     lfr.type = NGTCP2_FRAME_PADDING;
-    lfr.padding.len = ngtcp2_ppe_padding(&ppe);
-    ngtcp2_log_tx_fr(&conn->log, &hd, &lfr);
+    lfr.padding.len = ngtcp2_ppe_padding(ppe);
+    ngtcp2_log_tx_fr(&conn->log, hd, &lfr);
   } else {
     lfr.type = NGTCP2_FRAME_PADDING;
-    lfr.padding.len = ngtcp2_ppe_padding_hp_sample(&ppe);
+    lfr.padding.len = ngtcp2_ppe_padding_hp_sample(ppe);
     if (lfr.padding.len) {
-      ngtcp2_log_tx_fr(&conn->log, &hd, &lfr);
+      ngtcp2_log_tx_fr(&conn->log, hd, &lfr);
     }
   }
 
-  nwrite = ngtcp2_ppe_final(&ppe, NULL);
+  nwrite = ngtcp2_ppe_final(ppe, NULL);
   if (nwrite < 0) {
     assert(ngtcp2_err_is_fatal((int)nwrite));
     return nwrite;
   }
 
   if (*pfrc != pktns->tx.frq) {
-    rv = ngtcp2_rtb_entry_new(&ent, &hd, NULL, ts, (size_t)nwrite,
+    rv = ngtcp2_rtb_entry_new(&ent, hd, NULL, ts, (size_t)nwrite,
                               rtb_entry_flags, conn->mem);
     if (rv != 0) {
       assert(ngtcp2_err_is_fatal((int)nwrite));
@@ -2608,20 +2655,9 @@ static ssize_t conn_write_pkt(ngtcp2_conn *conn, uint8_t *dest, size_t destlen,
       ngtcp2_rtb_entry_del(ent, conn->mem);
       return rv;
     }
-
-    if (send_stream) {
-      data_strm->tx.offset += ndatalen;
-      conn->tx.offset += ndatalen;
-
-      if (fin) {
-        ngtcp2_strm_shutdown(data_strm, NGTCP2_STRM_FLAG_SHUT_WR);
-      }
-    }
   }
 
-  if (pdatalen && send_stream) {
-    *pdatalen = (ssize_t)ndatalen;
-  }
+  conn->flags &= (uint16_t)~NGTCP2_CONN_FLAG_PPE_PENDING;
 
   ++pktns->tx.last_pkt_num;
 
@@ -2981,7 +3017,7 @@ static ssize_t conn_write_probe_pkt(ngtcp2_conn *conn, uint8_t *dest,
 
   /* a probe packet is not blocked by cwnd. */
   nwrite = conn_write_pkt(conn, dest, destlen, pdatalen, NGTCP2_PKT_SHORT, strm,
-                          fin, datav, datavcnt, /* require_padding = */ 0, ts);
+                          fin, datav, datavcnt, NGTCP2_WRITE_PKT_FLAG_NONE, ts);
   if (nwrite == 0 || nwrite == NGTCP2_ERR_STREAM_DATA_BLOCKED) {
     nwrite = conn_write_probe_ping(conn, dest, destlen, ts);
   }
@@ -3321,84 +3357,11 @@ static int conn_peer_has_unused_cid(ngtcp2_conn *conn) {
 
 ssize_t ngtcp2_conn_write_pkt(ngtcp2_conn *conn, ngtcp2_path *path,
                               uint8_t *dest, size_t destlen, ngtcp2_tstamp ts) {
-  ssize_t nwrite;
-  uint64_t cwnd;
-  ngtcp2_pktns *pktns = &conn->pktns;
-  size_t origlen = destlen;
-  int rv;
-
-  conn->log.last_ts = ts;
-
-  if (pktns->tx.last_pkt_num == NGTCP2_MAX_PKT_NUM) {
-    return NGTCP2_ERR_PKT_NUM_EXHAUSTED;
-  }
-
-  switch (conn->state) {
-  case NGTCP2_CS_CLIENT_INITIAL:
-  case NGTCP2_CS_CLIENT_WAIT_HANDSHAKE:
-  case NGTCP2_CS_CLIENT_TLS_HANDSHAKE_FAILED:
-  case NGTCP2_CS_SERVER_INITIAL:
-  case NGTCP2_CS_SERVER_WAIT_HANDSHAKE:
-  case NGTCP2_CS_SERVER_TLS_HANDSHAKE_FAILED:
-    return NGTCP2_ERR_INVALID_STATE;
-  case NGTCP2_CS_POST_HANDSHAKE:
-    rv = conn_remove_retired_connection_id(conn, ts);
-    if (rv != 0) {
-      return rv;
-    }
-
-    nwrite = conn_write_path_response(conn, path, dest, destlen, ts);
-    if (nwrite) {
-      return nwrite;
-    }
-
-    if (conn->pv && conn_peer_has_unused_cid(conn)) {
-      nwrite = conn_write_path_challenge(conn, path, dest, destlen, ts);
-      if (nwrite) {
-        return nwrite;
-      }
-    }
-
-    cwnd = conn_cwnd_left(conn);
-    destlen = ngtcp2_min(destlen, cwnd);
-
-    if (path) {
-      ngtcp2_path_copy(path, &conn->dcid.current.ps.path);
-    }
-
-    if (conn_handshake_remnants_left(conn)) {
-      nwrite = conn_write_handshake_pkts(conn, dest, destlen, 0, ts);
-      if (nwrite) {
-        return nwrite;
-      }
-    }
-    nwrite = conn_write_handshake_ack_pkts(conn, dest, origlen, ts);
-    if (nwrite) {
-      return nwrite;
-    }
-
-    if (conn->rcs.probe_pkt_left) {
-      return conn_write_probe_pkt(conn, dest, origlen, NULL, NULL, 0, NULL, 0,
-                                  ts);
-    }
-
-    nwrite = conn_write_pkt(conn, dest, destlen, NULL, NGTCP2_PKT_SHORT, NULL,
-                            0, NULL, 0, /* require_padding = */ 0, ts);
-    if (nwrite < 0) {
-      assert(nwrite != NGTCP2_ERR_NOBUF);
-      return nwrite;
-    }
-    if (nwrite) {
-      return nwrite;
-    }
-    return conn_write_protected_ack_pkt(conn, dest, origlen, ts);
-  case NGTCP2_CS_CLOSING:
-    return NGTCP2_ERR_CLOSING;
-  case NGTCP2_CS_DRAINING:
-    return NGTCP2_ERR_DRAINING;
-  default:
-    return 0;
-  }
+  return ngtcp2_conn_writev_stream(
+      conn, path, dest, destlen,
+      /* pdatalen = */ NULL, NGTCP2_WRITE_STREAM_FLAG_NONE,
+      /* stream_id = */ -1,
+      /* fin = */ 0, /* datav = */ NULL, /* datavcnt = */ 0, ts);
 }
 
 /*
@@ -6667,7 +6630,7 @@ int ngtcp2_conn_read_pkt(ngtcp2_conn *conn, const ngtcp2_path *path,
   case NGTCP2_CS_SERVER_INITIAL:
   case NGTCP2_CS_SERVER_WAIT_HANDSHAKE:
   case NGTCP2_CS_SERVER_TLS_HANDSHAKE_FAILED:
-    return NGTCP2_ERR_INVALID_STATE;
+    return ngtcp2_conn_read_handshake(conn, path, pkt, pktlen, ts);
   case NGTCP2_CS_CLOSING:
     return NGTCP2_ERR_CLOSING;
   case NGTCP2_CS_DRAINING:
@@ -6716,13 +6679,6 @@ int ngtcp2_conn_read_handshake(ngtcp2_conn *conn, const ngtcp2_path *path,
   int rv;
   ngtcp2_pktns *hs_pktns = &conn->hs_pktns;
 
-  conn->log.last_ts = ts;
-
-  if (pktlen > 0) {
-    ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_CON, "recv packet len=%zu",
-                    pktlen);
-  }
-
   switch (conn->state) {
   case NGTCP2_CS_CLIENT_INITIAL:
     /* TODO Better to log something when we ignore input */
@@ -6884,7 +6840,7 @@ static int conn_select_preferred_addr(ngtcp2_conn *conn) {
 static ssize_t conn_retransmit_retry_early(ngtcp2_conn *conn, uint8_t *dest,
                                            size_t destlen, ngtcp2_tstamp ts) {
   return conn_write_pkt(conn, dest, destlen, NULL, NGTCP2_PKT_0RTT, NULL, 0,
-                        NULL, 0, /* require_padding = */ 0, ts);
+                        NULL, 0, NGTCP2_WRITE_PKT_FLAG_NONE, ts);
 }
 
 /*
@@ -6921,12 +6877,6 @@ static ssize_t conn_write_handshake(ngtcp2_conn *conn, uint8_t *dest,
   ngtcp2_rcvry_stat *rcs = &conn->rcs;
   size_t pending_early_datalen;
 
-  conn->log.last_ts = ts;
-
-  if (conn_check_pkt_num_exhausted(conn)) {
-    return NGTCP2_ERR_PKT_NUM_EXHAUSTED;
-  }
-
   cwnd = conn_cwnd_left(conn);
   destlen = ngtcp2_min(destlen, cwnd);
 
@@ -7135,24 +7085,21 @@ ssize_t ngtcp2_conn_write_handshake(ngtcp2_conn *conn, uint8_t *dest,
 
 ssize_t ngtcp2_conn_client_write_handshake(ngtcp2_conn *conn, uint8_t *dest,
                                            size_t destlen, ssize_t *pdatalen,
-                                           int64_t stream_id, uint8_t fin,
-                                           const ngtcp2_vec *datav,
+                                           uint32_t flags, int64_t stream_id,
+                                           uint8_t fin, const ngtcp2_vec *datav,
                                            size_t datavcnt, ngtcp2_tstamp ts) {
   ngtcp2_strm *strm = NULL;
   int send_stream = 0;
   ssize_t spktlen, early_spktlen;
   uint64_t cwnd;
-  int require_padding;
   int was_client_initial;
   size_t datalen = ngtcp2_vec_len(datav, datavcnt);
   size_t early_datalen = 0;
+  uint8_t wflags = NGTCP2_WRITE_PKT_FLAG_NONE;
+  int ppe_pending = (conn->flags & NGTCP2_CONN_FLAG_PPE_PENDING) != 0;
 
   assert(!conn->server);
 
-  if (pdatalen) {
-    *pdatalen = -1;
-  }
-
   /* conn->early.ckm might be created in the first call of
      conn_handshake().  Check it later. */
   if (stream_id != -1 &&
@@ -7180,21 +7127,34 @@ ssize_t ngtcp2_conn_client_write_handshake(ngtcp2_conn *conn, uint8_t *dest,
     }
   }
 
-  was_client_initial = conn->state == NGTCP2_CS_CLIENT_INITIAL;
-  spktlen = conn_write_handshake(conn, dest, destlen, early_datalen, ts);
+  if (!ppe_pending) {
+    was_client_initial = conn->state == NGTCP2_CS_CLIENT_INITIAL;
+    spktlen = conn_write_handshake(conn, dest, destlen, early_datalen, ts);
 
-  if (spktlen < 0) {
-    return spktlen;
-  }
+    if (spktlen < 0) {
+      return spktlen;
+    }
 
-  if (conn->pktns.crypto.tx.ckm || !conn->early.ckm || !send_stream) {
-    return spktlen;
+    if (conn->pktns.crypto.tx.ckm || !conn->early.ckm || !send_stream) {
+      return spktlen;
+    }
+  } else {
+    assert(!conn->pktns.crypto.tx.ckm);
+    assert(conn->early.ckm);
+
+    was_client_initial = conn->pkt.was_client_initial;
+    spktlen = conn->pkt.hs_spktlen;
   }
 
   /* If spktlen > 0, we are making a compound packet.  If Initial
      packet is written, we have to pad bytes to 0-RTT packet. */
 
-  require_padding = spktlen && was_client_initial;
+  if (spktlen && was_client_initial) {
+    wflags |= NGTCP2_WRITE_PKT_FLAG_REQUIRE_PADDING;
+  }
+  if (flags & NGTCP2_WRITE_STREAM_FLAG_MORE) {
+    wflags |= NGTCP2_WRITE_PKT_FLAG_STREAM_MORE;
+  }
 
   cwnd = conn_cwnd_left(conn);
 
@@ -7202,13 +7162,17 @@ ssize_t ngtcp2_conn_client_write_handshake(ngtcp2_conn *conn, uint8_t *dest,
   destlen -= (size_t)spktlen;
   destlen = ngtcp2_min(destlen, cwnd);
 
-  early_spktlen =
-      conn_write_pkt(conn, dest, destlen, pdatalen, NGTCP2_PKT_0RTT, strm, fin,
-                     datav, datavcnt, require_padding, ts);
+  early_spktlen = conn_write_pkt(conn, dest, destlen, pdatalen, NGTCP2_PKT_0RTT,
+                                 strm, fin, datav, datavcnt, wflags, ts);
 
   if (early_spktlen < 0) {
-    if (early_spktlen == NGTCP2_ERR_STREAM_DATA_BLOCKED) {
+    switch (early_spktlen) {
+    case NGTCP2_ERR_STREAM_DATA_BLOCKED:
       return spktlen;
+    case NGTCP2_ERR_WRITE_STREAM_MORE:
+      conn->pkt.was_client_initial = was_client_initial;
+      conn->pkt.hs_spktlen = spktlen;
+      break;
     }
     return early_spktlen;
   }
@@ -7750,24 +7714,26 @@ ngtcp2_strm *ngtcp2_conn_find_stream(ngtcp2_conn *conn, int64_t stream_id) {
 
 ssize_t ngtcp2_conn_write_stream(ngtcp2_conn *conn, ngtcp2_path *path,
                                  uint8_t *dest, size_t destlen,
-                                 ssize_t *pdatalen, int64_t stream_id,
-                                 uint8_t fin, const uint8_t *data,
-                                 size_t datalen, ngtcp2_tstamp ts) {
+                                 ssize_t *pdatalen, uint32_t flags,
+                                 int64_t stream_id, uint8_t fin,
+                                 const uint8_t *data, size_t datalen,
+                                 ngtcp2_tstamp ts) {
   ngtcp2_vec datav;
 
   datav.len = datalen;
   datav.base = (uint8_t *)data;
 
-  return ngtcp2_conn_writev_stream(conn, path, dest, destlen, pdatalen,
+  return ngtcp2_conn_writev_stream(conn, path, dest, destlen, pdatalen, flags,
                                    stream_id, fin, &datav, 1, ts);
 }
 
 ssize_t ngtcp2_conn_writev_stream(ngtcp2_conn *conn, ngtcp2_path *path,
                                   uint8_t *dest, size_t destlen,
-                                  ssize_t *pdatalen, int64_t stream_id,
-                                  uint8_t fin, const ngtcp2_vec *datav,
-                                  size_t datavcnt, ngtcp2_tstamp ts) {
-  ngtcp2_strm *strm;
+                                  ssize_t *pdatalen, uint32_t flags,
+                                  int64_t stream_id, uint8_t fin,
+                                  const ngtcp2_vec *datav, size_t datavcnt,
+                                  ngtcp2_tstamp ts) {
+  ngtcp2_strm *strm = NULL;
   ssize_t nwrite;
   uint64_t cwnd;
   ngtcp2_pktns *pktns = &conn->pktns;
@@ -7775,6 +7741,8 @@ ssize_t ngtcp2_conn_writev_stream(ngtcp2_conn *conn, ngtcp2_path *path,
   size_t server_hs_tx_left;
   ngtcp2_rcvry_stat *rcs = &conn->rcs;
   int rv;
+  uint8_t wflags = NGTCP2_WRITE_PKT_FLAG_NONE;
+  int ppe_pending = (conn->flags & NGTCP2_CONN_FLAG_PPE_PENDING) != 0;
 
   conn->log.last_ts = ts;
 
@@ -7783,10 +7751,37 @@ ssize_t ngtcp2_conn_writev_stream(ngtcp2_conn *conn, ngtcp2_path *path,
   }
 
   switch (conn->state) {
+  case NGTCP2_CS_CLIENT_INITIAL:
+  case NGTCP2_CS_CLIENT_WAIT_HANDSHAKE:
+  case NGTCP2_CS_CLIENT_TLS_HANDSHAKE_FAILED:
+    if (path) {
+      ngtcp2_path_copy(path, &conn->dcid.current.ps.path);
+    }
+    return ngtcp2_conn_client_write_handshake(conn, dest, destlen, pdatalen,
+                                              flags, stream_id, fin, datav,
+                                              datavcnt, ts);
+  case NGTCP2_CS_SERVER_INITIAL:
+  case NGTCP2_CS_SERVER_WAIT_HANDSHAKE:
+  case NGTCP2_CS_SERVER_TLS_HANDSHAKE_FAILED:
+    if (path) {
+      ngtcp2_path_copy(path, &conn->dcid.current.ps.path);
+    }
+    nwrite = ngtcp2_conn_write_handshake(conn, dest, destlen, ts);
+    if (nwrite) {
+      return nwrite;
+    }
+    if (conn->state != NGTCP2_CS_POST_HANDSHAKE) {
+      return 0;
+    }
+    break;
+  case NGTCP2_CS_POST_HANDSHAKE:
+    break;
   case NGTCP2_CS_CLOSING:
     return NGTCP2_ERR_CLOSING;
   case NGTCP2_CS_DRAINING:
     return NGTCP2_ERR_DRAINING;
+  default:
+    return 0;
   }
 
   if (conn_check_pkt_num_exhausted(conn)) {
@@ -7798,25 +7793,29 @@ ssize_t ngtcp2_conn_writev_stream(ngtcp2_conn *conn, ngtcp2_path *path,
     return rv;
   }
 
-  strm = ngtcp2_conn_find_stream(conn, stream_id);
-  if (strm == NULL) {
-    return NGTCP2_ERR_STREAM_NOT_FOUND;
-  }
-
-  if (strm->flags & NGTCP2_STRM_FLAG_SHUT_WR) {
-    return NGTCP2_ERR_STREAM_SHUT_WR;
-  }
+  if (stream_id != -1) {
+    strm = ngtcp2_conn_find_stream(conn, stream_id);
+    if (strm == NULL) {
+      return NGTCP2_ERR_STREAM_NOT_FOUND;
+    }
 
-  nwrite = conn_write_path_response(conn, path, dest, destlen, ts);
-  if (nwrite) {
-    return nwrite;
+    if (strm->flags & NGTCP2_STRM_FLAG_SHUT_WR) {
+      return NGTCP2_ERR_STREAM_SHUT_WR;
+    }
   }
 
-  if (conn->pv && conn_peer_has_unused_cid(conn)) {
-    nwrite = conn_write_path_challenge(conn, path, dest, destlen, ts);
+  if (!ppe_pending) {
+    nwrite = conn_write_path_response(conn, path, dest, destlen, ts);
     if (nwrite) {
       return nwrite;
     }
+
+    if (conn->pv && conn_peer_has_unused_cid(conn)) {
+      nwrite = conn_write_path_challenge(conn, path, dest, destlen, ts);
+      if (nwrite) {
+        return nwrite;
+      }
+    }
   }
 
   cwnd = conn_cwnd_left(conn);
@@ -7825,6 +7824,7 @@ ssize_t ngtcp2_conn_writev_stream(ngtcp2_conn *conn, ngtcp2_path *path,
   if (conn->server) {
     server_hs_tx_left = conn_server_hs_tx_left(conn);
     if (server_hs_tx_left == 0) {
+      assert(!ppe_pending);
       if (rcs->loss_detection_timer) {
         ngtcp2_log_info(&conn->log, NGTCP2_LOG_EVENT_RCV,
                         "loss detection timer canceled");
@@ -7839,47 +7839,45 @@ ssize_t ngtcp2_conn_writev_stream(ngtcp2_conn *conn, ngtcp2_path *path,
     ngtcp2_path_copy(path, &conn->dcid.current.ps.path);
   }
 
-  if (conn_handshake_remnants_left(conn)) {
-    nwrite = conn_write_handshake_pkts(conn, dest, destlen, 0, ts);
+  if (!ppe_pending) {
+    if (conn_handshake_remnants_left(conn)) {
+      nwrite = conn_write_handshake_pkts(conn, dest, destlen, 0, ts);
+      if (nwrite) {
+        return nwrite;
+      }
+    }
+    nwrite = conn_write_handshake_ack_pkts(conn, dest, origlen, ts);
     if (nwrite) {
       return nwrite;
     }
   }
-  nwrite = conn_write_handshake_ack_pkts(conn, dest, origlen, ts);
-  if (nwrite) {
-    return nwrite;
+
+  if (flags & NGTCP2_WRITE_STREAM_FLAG_MORE) {
+    wflags |= NGTCP2_WRITE_PKT_FLAG_STREAM_MORE;
   }
 
-  if (pktns->crypto.tx.ckm) {
-    if (conn->rcs.probe_pkt_left) {
-      return conn_write_probe_pkt(conn, dest, origlen, pdatalen, strm, fin,
-                                  datav, datavcnt, ts);
-    }
+  assert(pktns->crypto.tx.ckm);
 
-    nwrite =
-        conn_write_pkt(conn, dest, destlen, pdatalen, NGTCP2_PKT_SHORT, strm,
-                       fin, datav, datavcnt, /* require_padding = */ 0, ts);
-    if (nwrite < 0) {
-      assert(nwrite != NGTCP2_ERR_NOBUF);
-      return nwrite;
-    }
-    if (nwrite == 0) {
-      return conn_write_protected_ack_pkt(conn, dest, origlen, ts);
-    }
-    return nwrite;
+  if (ppe_pending) {
+    return conn_write_pkt(conn, dest, destlen, pdatalen, NGTCP2_PKT_SHORT, strm,
+                          fin, datav, datavcnt, wflags, ts);
   }
 
-  /* Send STREAM frame in 0-RTT packet. */
-  if (conn->server || !conn->early.ckm) {
-    return NGTCP2_ERR_NOKEY;
+  if (conn->rcs.probe_pkt_left) {
+    return conn_write_probe_pkt(conn, dest, origlen, pdatalen, strm, fin, datav,
+                                datavcnt, ts);
   }
 
-  if (conn->flags & NGTCP2_CONN_FLAG_EARLY_DATA_REJECTED) {
-    return NGTCP2_ERR_EARLY_DATA_REJECTED;
+  nwrite = conn_write_pkt(conn, dest, destlen, pdatalen, NGTCP2_PKT_SHORT, strm,
+                          fin, datav, datavcnt, wflags, ts);
+  if (nwrite < 0) {
+    assert(nwrite != NGTCP2_ERR_NOBUF);
+    return nwrite;
   }
-
-  return conn_write_pkt(conn, dest, destlen, pdatalen, NGTCP2_PKT_0RTT, strm,
-                        fin, datav, datavcnt, /* require_padding = */ 0, ts);
+  if (nwrite == 0) {
+    return conn_write_protected_ack_pkt(conn, dest, origlen, ts);
+  }
+  return nwrite;
 }
 
 ssize_t ngtcp2_conn_write_connection_close(ngtcp2_conn *conn, ngtcp2_path *path,
diff --git a/lib/ngtcp2_conn.h b/lib/ngtcp2_conn.h
index ac3195c407e877cb3a609afbccd861fdab394df7..f03134ae49a8128f788b3503793714e032643dfd 100644
--- a/lib/ngtcp2_conn.h
+++ b/lib/ngtcp2_conn.h
@@ -46,6 +46,7 @@
 #include "ngtcp2_pv.h"
 #include "ngtcp2_cid.h"
 #include "ngtcp2_buf.h"
+#include "ngtcp2_ppe.h"
 
 typedef enum {
   /* Client specific handshake states */
@@ -183,6 +184,10 @@ typedef enum {
      endpoint has initiated key update and waits for the remote
      endpoint to update key. */
   NGTCP2_CONN_FLAG_WAIT_FOR_REMOTE_KEY_UPDATE = 0x0800,
+  /* NGTCP2_CONN_FLAG_PPE_PENDING is set when
+     NGTCP2_WRITE_STREAM_FLAG_MORE is used and the intermediate state
+     of ngtcp2_ppe is stored in pkt struct of ngtcp2_conn. */
+  NGTCP2_CONN_FLAG_PPE_PENDING = 0x1000,
 } ngtcp2_conn_flag;
 
 typedef struct {
@@ -425,6 +430,19 @@ struct ngtcp2_conn {
     ngtcp2_array decrypt_buf;
   } crypto;
 
+  /* pkt contains the packet intermediate construction data to support
+     NGTCP2_WRITE_STREAM_FLAG_MORE */
+  struct {
+    ngtcp2_crypto_ctx ctx;
+    ngtcp2_pkt_hd hd;
+    ngtcp2_ppe ppe;
+    ngtcp2_frame_chain **pfrc;
+    int pkt_empty;
+    uint8_t rtb_entry_flags;
+    int was_client_initial;
+    ssize_t hs_spktlen;
+  } pkt;
+
   ngtcp2_map strms;
   ngtcp2_rcvry_stat rcs;
   ngtcp2_cc_stat ccs;
@@ -449,6 +467,102 @@ struct ngtcp2_conn {
   int server;
 };
 
+/**
+ * @function
+ *
+ * `ngtcp2_conn_read_handshake` performs QUIC cryptographic handshake
+ * by reading given data.  |pkt| points to the buffer to read and
+ * |pktlen| is the length of the buffer.  |path| is the network path.
+ *
+ * The application should call `ngtcp2_conn_write_handshake` (or
+ * `ngtcp2_conn_client_write_handshake` for client session) to make
+ * handshake go forward after calling this function.
+ *
+ * Application should call this function until
+ * `ngtcp2_conn_get_handshake_completed` returns nonzero.  After the
+ * completion of handshake, `ngtcp2_conn_read_pkt` and
+ * `ngtcp2_conn_write_pkt` should be called instead.
+ *
+ * This function must not be called from inside the callback
+ * functions.
+ *
+ * This function returns 0 if it succeeds, or one of the following
+ * negative error codes: (TBD).
+ */
+int ngtcp2_conn_read_handshake(ngtcp2_conn *conn, const ngtcp2_path *path,
+                               const uint8_t *pkt, size_t pktlen,
+                               ngtcp2_tstamp ts);
+
+/**
+ * @function
+ *
+ * `ngtcp2_conn_write_handshake` performs QUIC cryptographic handshake
+ * by writing handshake packets.  It may write a packet in the given
+ * buffer pointed by |dest| whose capacity is given as |destlen|.
+ * Application must ensure that the buffer pointed by |dest| is not
+ * empty.
+ *
+ * Application should keep calling this function repeatedly until it
+ * returns zero, or negative error code.
+ *
+ * Application should call this function until
+ * `ngtcp2_conn_get_handshake_completed` returns nonzero.  After the
+ * completion of handshake, `ngtcp2_conn_read_pkt` and
+ * `ngtcp2_conn_write_pkt` should be called instead.
+ *
+ * During handshake, application can send 0-RTT data (or its response)
+ * using `ngtcp2_conn_write_stream`.
+ * `ngtcp2_conn_client_write_handshake` is generally efficient because
+ * it can coalesce Handshake packet and 0-RTT packet into one UDP
+ * packet.
+ *
+ * This function returns 0 if it cannot write any frame because buffer
+ * is too small, or packet is congestion limited.  Application should
+ * keep reading and wait for congestion window to grow.
+ *
+ * This function must not be called from inside the callback
+ * functions.
+ *
+ * This function returns the number of bytes written to the buffer
+ * pointed by |dest| if it succeeds, or one of the following negative
+ * error codes: (TBD).
+ */
+ssize_t ngtcp2_conn_write_handshake(ngtcp2_conn *conn, uint8_t *dest,
+                                    size_t destlen, ngtcp2_tstamp ts);
+
+/**
+ * @function
+ *
+ * `ngtcp2_conn_client_write_handshake` is just like
+ * `ngtcp2_conn_write_handshake`, but it is for client only, and can
+ * write 0-RTT data.  This function can coalesce handshake packet and
+ * 0-RTT packet into single UDP packet, thus it is generally more
+ * efficient than the combination of `ngtcp2_conn_write_handshake` and
+ * `ngtcp2_conn_write_stream`.
+ *
+ * |stream_id|, |fin|, |datav|, and |datavcnt| are stream identifier
+ * to which 0-RTT data is sent, whether it is a last data chunk in
+ * this stream, a vector of 0-RTT data, and its number of elements
+ * respectively.  If there is no 0RTT data to send, pass negative
+ * integer to |stream_id|.  The amount of 0RTT data sent is assigned
+ * to |*pdatalen|.  If no data is sent, -1 is assigned.  Note that 0
+ * length STREAM frame is allowed in QUIC, so 0 might be assigned to
+ * |*pdatalen|.
+ *
+ * This function returns 0 if it cannot write any frame because buffer
+ * is too small, or packet is congestion limited.  Application should
+ * keep reading and wait for congestion window to grow.
+ *
+ * This function returns the number of bytes written to the buffer
+ * pointed by |dest| if it succeeds, or one of the following negative
+ * error codes: (TBD).
+ */
+ssize_t ngtcp2_conn_client_write_handshake(ngtcp2_conn *conn, uint8_t *dest,
+                                           size_t destlen, ssize_t *pdatalen,
+                                           uint32_t flags, int64_t stream_id,
+                                           uint8_t fin, const ngtcp2_vec *datav,
+                                           size_t datavcnt, ngtcp2_tstamp ts);
+
 /*
  * ngtcp2_conn_sched_ack stores packet number |pkt_num| and its
  * reception timestamp |ts| in order to send its ACK.
diff --git a/lib/ngtcp2_err.c b/lib/ngtcp2_err.c
index 082e51f44d5fae7a5cecd7eb42423eb08bda7ea6..3d324ea24fa3ca2fb905b912fd1effed53691968 100644
--- a/lib/ngtcp2_err.c
+++ b/lib/ngtcp2_err.c
@@ -96,6 +96,8 @@ const char *ngtcp2_strerror(int liberr) {
     return "ERR_INTERNAL";
   case NGTCP2_ERR_CRYPTO_BUFFER_EXCEEDED:
     return "ERR_CRYPTO_BUFFER_EXCEEDED";
+  case NGTCP2_ERR_WRITE_STREAM_MORE:
+    return "ERR_WRITE_STREAM_MORE";
   default:
     return "(unknown)";
   }
diff --git a/tests/main.c b/tests/main.c
index d35f4a8510248312a357687f8c86234ee6c8e199..4e25746a05454b75d215a98b6873efd334a1c762 100644
--- a/tests/main.c
+++ b/tests/main.c
@@ -198,8 +198,6 @@ int main() {
       !CU_add_test(pSuite, "conn_handshake", test_ngtcp2_conn_handshake) ||
       !CU_add_test(pSuite, "conn_handshake_error",
                    test_ngtcp2_conn_handshake_error) ||
-      !CU_add_test(pSuite, "conn_client_write_handshake",
-                   test_ngtcp2_conn_client_write_handshake) ||
       !CU_add_test(pSuite, "conn_retransmit_protected",
                    test_ngtcp2_conn_retransmit_protected) ||
       !CU_add_test(pSuite, "conn_send_max_stream_data",
@@ -226,7 +224,7 @@ int main() {
       !CU_add_test(pSuite, "conn_server_path_validation",
                    test_ngtcp2_conn_server_path_validation) ||
       !CU_add_test(pSuite, "conn_client_connection_migration",
-                   test_ngtcp2_conn_client_write_handshake) ||
+                   test_ngtcp2_conn_client_connection_migration) ||
       !CU_add_test(pSuite, "conn_recv_path_challenge",
                    test_ngtcp2_conn_recv_path_challenge) ||
       !CU_add_test(pSuite, "conn_key_update", test_ngtcp2_conn_key_update) ||
diff --git a/tests/ngtcp2_conn_test.c b/tests/ngtcp2_conn_test.c
index ea08b1bd1a9122293614228bf449fc493a221625..d3c644fa2d82a2156fd8c45d3015e6974bb5612e 100644
--- a/tests/ngtcp2_conn_test.c
+++ b/tests/ngtcp2_conn_test.c
@@ -643,8 +643,9 @@ void test_ngtcp2_conn_stream_open_close(void) {
   CU_ASSERT(fr.stream.offset == strm->rx.last_offset);
   CU_ASSERT(fr.stream.offset == ngtcp2_strm_rx_offset(strm));
 
-  spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL, 4, 1,
-                                     NULL, 0, 3);
+  spktlen =
+      ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
+                               NGTCP2_WRITE_STREAM_FLAG_NONE, 4, 1, NULL, 0, 3);
 
   CU_ASSERT(spktlen > 0);
 
@@ -807,27 +808,31 @@ void test_ngtcp2_conn_stream_tx_flow_control(void) {
 
   strm = ngtcp2_conn_find_stream(conn, stream_id);
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), &nwrite,
-                                     stream_id, 0, null_data, 1024, 1);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 1024, 1);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(1024 == nwrite);
   CU_ASSERT(1024 == strm->tx.offset);
 
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), &nwrite,
-                                     stream_id, 0, null_data, 1024, 2);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 1024, 2);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(1023 == nwrite);
   CU_ASSERT(2047 == strm->tx.offset);
 
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), &nwrite,
-                                     stream_id, 0, null_data, 1024, 3);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 1024, 3);
 
   CU_ASSERT(NGTCP2_ERR_STREAM_DATA_BLOCKED == spktlen);
 
   /* We can write 0 length STREAM frame */
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), &nwrite,
-                                     stream_id, 0, null_data, 0, 3);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 0, 3);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(0 == nwrite);
@@ -845,7 +850,8 @@ void test_ngtcp2_conn_stream_tx_flow_control(void) {
   CU_ASSERT(2048 == strm->tx.max_offset);
 
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), &nwrite,
-                                     stream_id, 0, null_data, 1024, 5);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 1024, 5);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(1 == nwrite);
@@ -863,7 +869,8 @@ void test_ngtcp2_conn_stream_tx_flow_control(void) {
   CU_ASSERT(0 == rv);
 
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), &nwrite,
-                                     stream_id, 1, null_data, 1024, 1);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     1, null_data, 1024, 1);
 
   CU_ASSERT(0 == spktlen);
   CU_ASSERT(-1 == nwrite);
@@ -981,28 +988,32 @@ void test_ngtcp2_conn_tx_flow_control(void) {
   CU_ASSERT(0 == rv);
 
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), &nwrite,
-                                     stream_id, 0, null_data, 1024, 1);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 1024, 1);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(1024 == nwrite);
   CU_ASSERT(1024 == conn->tx.offset);
 
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), &nwrite,
-                                     stream_id, 0, null_data, 1023, 2);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 1023, 2);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(1023 == nwrite);
   CU_ASSERT(1024 + 1023 == conn->tx.offset);
 
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), &nwrite,
-                                     stream_id, 0, null_data, 1024, 3);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 1024, 3);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(1 == nwrite);
   CU_ASSERT(2048 == conn->tx.offset);
 
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), &nwrite,
-                                     stream_id, 0, null_data, 1024, 4);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 1024, 4);
 
   CU_ASSERT(NGTCP2_ERR_STREAM_DATA_BLOCKED == spktlen);
   CU_ASSERT(-1 == nwrite);
@@ -1018,7 +1029,8 @@ void test_ngtcp2_conn_tx_flow_control(void) {
   CU_ASSERT(3072 == conn->tx.max_offset);
 
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), &nwrite,
-                                     stream_id, 0, null_data, 1024, 4);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 1024, 4);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(1024 == nwrite);
@@ -1051,7 +1063,8 @@ void test_ngtcp2_conn_shutdown_stream_write(void) {
   setup_default_client(&conn);
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
-  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL, stream_id, 0,
+  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
+                           NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id, 0,
                            null_data, 1239, 1);
   rv = ngtcp2_conn_shutdown_stream_write(conn, stream_id, NGTCP2_APP_ERR01);
 
@@ -1190,8 +1203,9 @@ void test_ngtcp2_conn_recv_reset_stream(void) {
 
   CU_ASSERT(0 == rv);
 
-  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL, 4, 0, null_data,
-                           354, 2);
+  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
+                           NGTCP2_WRITE_STREAM_FLAG_NONE, 4, 0, null_data, 354,
+                           2);
 
   fr.type = NGTCP2_FRAME_RESET_STREAM;
   fr.reset_stream.stream_id = 4;
@@ -1227,8 +1241,9 @@ void test_ngtcp2_conn_recv_reset_stream(void) {
 
   CU_ASSERT(0 == rv);
 
-  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL, 4, 0, null_data,
-                           354, 2);
+  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
+                           NGTCP2_WRITE_STREAM_FLAG_NONE, 4, 0, null_data, 354,
+                           2);
   ngtcp2_conn_shutdown_stream_read(conn, 4, NGTCP2_APP_ERR01);
   ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), 3);
 
@@ -1262,8 +1277,9 @@ void test_ngtcp2_conn_recv_reset_stream(void) {
 
   CU_ASSERT(0 == rv);
 
-  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL, 4, 0, null_data,
-                           354, 2);
+  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
+                           NGTCP2_WRITE_STREAM_FLAG_NONE, 4, 0, null_data, 354,
+                           2);
   ngtcp2_conn_shutdown_stream_write(conn, 4, NGTCP2_APP_ERR01);
   ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), 3);
 
@@ -1309,8 +1325,9 @@ void test_ngtcp2_conn_recv_reset_stream(void) {
 
   CU_ASSERT(0 == rv);
 
-  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL, 4, 0, null_data,
-                           354, 2);
+  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
+                           NGTCP2_WRITE_STREAM_FLAG_NONE, 4, 0, null_data, 354,
+                           2);
 
   fr.type = NGTCP2_FRAME_STOP_SENDING;
   fr.stop_sending.stream_id = 4;
@@ -1615,7 +1632,8 @@ void test_ngtcp2_conn_recv_stop_sending(void) {
   setup_default_client(&conn);
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
-  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL, stream_id, 0,
+  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
+                           NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id, 0,
                            null_data, 333, ++t);
 
   fr.type = NGTCP2_FRAME_STOP_SENDING;
@@ -1649,7 +1667,8 @@ void test_ngtcp2_conn_recv_stop_sending(void) {
   setup_default_client(&conn);
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
-  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL, stream_id, 0,
+  ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
+                           NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id, 0,
                            null_data, 333, ++t);
 
   fr.type = NGTCP2_FRAME_RESET_STREAM;
@@ -1859,7 +1878,8 @@ void test_ngtcp2_conn_short_pkt_type(void) {
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
-                                     stream_id, 0, null_data, 19, 1);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 19, 1);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(pkt_decode_hd_short_mask(&hd, buf, (size_t)spktlen,
@@ -1875,7 +1895,8 @@ void test_ngtcp2_conn_short_pkt_type(void) {
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
-                                     stream_id, 0, null_data, 19, 1);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 19, 1);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(pkt_decode_hd_short_mask(&hd, buf, (size_t)spktlen,
@@ -1891,7 +1912,8 @@ void test_ngtcp2_conn_short_pkt_type(void) {
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
-                                     stream_id, 0, null_data, 19, 1);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 19, 1);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(pkt_decode_hd_short_mask(&hd, buf, (size_t)spktlen,
@@ -1907,7 +1929,8 @@ void test_ngtcp2_conn_short_pkt_type(void) {
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
-                                     stream_id, 0, null_data, 19, 1);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 19, 1);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(pkt_decode_hd_short_mask(&hd, buf, (size_t)spktlen,
@@ -1923,7 +1946,8 @@ void test_ngtcp2_conn_short_pkt_type(void) {
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
-                                     stream_id, 0, null_data, 19, 1);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 19, 1);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(pkt_decode_hd_short_mask(&hd, buf, (size_t)spktlen,
@@ -1939,7 +1963,8 @@ void test_ngtcp2_conn_short_pkt_type(void) {
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
-                                     stream_id, 0, null_data, 19, 1);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 19, 1);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(
@@ -1955,7 +1980,8 @@ void test_ngtcp2_conn_short_pkt_type(void) {
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
-                                     stream_id, 0, null_data, 19, 1);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 19, 1);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(
@@ -1971,7 +1997,8 @@ void test_ngtcp2_conn_short_pkt_type(void) {
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
-                                     stream_id, 0, null_data, 19, 1);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 19, 1);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(
@@ -1987,7 +2014,8 @@ void test_ngtcp2_conn_short_pkt_type(void) {
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
-                                     stream_id, 0, null_data, 19, 1);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 19, 1);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(
@@ -2003,7 +2031,8 @@ void test_ngtcp2_conn_short_pkt_type(void) {
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
-                                     stream_id, 0, null_data, 19, 1);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 19, 1);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(
@@ -2150,7 +2179,7 @@ void test_ngtcp2_conn_recv_retry(void) {
   setup_handshake_client(&conn);
   conn->callbacks.recv_retry = recv_retry;
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
   CU_ASSERT(spktlen > 0);
 
@@ -2164,12 +2193,11 @@ void test_ngtcp2_conn_recv_retry(void) {
 
     CU_ASSERT(spktlen > 0);
 
-    rv =
-        ngtcp2_conn_read_handshake(conn, &null_path, buf, (size_t)spktlen, ++t);
+    rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, (size_t)spktlen, ++t);
 
     CU_ASSERT(0 == rv);
 
-    spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+    spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
     if (i == 1) {
       /* Retry packet was ignored */
@@ -2188,7 +2216,7 @@ void test_ngtcp2_conn_recv_retry(void) {
   setup_handshake_client(&conn);
   conn->callbacks.recv_retry = recv_retry;
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
   CU_ASSERT(spktlen > 0);
 
@@ -2200,11 +2228,11 @@ void test_ngtcp2_conn_recv_retry(void) {
 
   CU_ASSERT(spktlen > 0);
 
-  rv = ngtcp2_conn_read_handshake(conn, &null_path, buf, (size_t)spktlen, ++t);
+  rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, (size_t)spktlen, ++t);
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
   CU_ASSERT(0 == spktlen);
 
@@ -2218,16 +2246,16 @@ void test_ngtcp2_conn_recv_retry(void) {
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_client_write_handshake(conn, buf, sizeof(buf), &datalen,
-                                               stream_id, 0,
-                                               null_datav(&datav, 219), 1, ++t);
+  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, sizeof(buf), &datalen,
+                                      NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                      0, null_datav(&datav, 219), 1, ++t);
 
   CU_ASSERT(sizeof(buf) == spktlen);
   CU_ASSERT(219 == datalen);
 
-  spktlen =
-      ngtcp2_conn_writev_stream(conn, NULL, buf, sizeof(buf), &datalen,
-                                stream_id, 0, null_datav(&datav, 119), 1, ++t);
+  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, sizeof(buf), &datalen,
+                                      NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                      0, null_datav(&datav, 119), 1, ++t);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(119 == datalen);
@@ -2240,11 +2268,11 @@ void test_ngtcp2_conn_recv_retry(void) {
 
   CU_ASSERT(spktlen > 0);
 
-  rv = ngtcp2_conn_read_handshake(conn, &null_path, buf, (size_t)spktlen, ++t);
+  rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, (size_t)spktlen, ++t);
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
   CU_ASSERT(spktlen > 219 + 119);
   CU_ASSERT(2 == conn->pktns.tx.last_pkt_num);
@@ -2255,7 +2283,8 @@ void test_ngtcp2_conn_recv_retry(void) {
 
   /* ngtcp2_conn_write_stream sends new 0RTT packet. */
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), &datalen,
-                                     stream_id, 0, null_data, 120, ++t);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 120, ++t);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(3 == conn->pktns.tx.last_pkt_num);
@@ -2394,11 +2423,11 @@ void test_ngtcp2_conn_handshake(void) {
       conn, buf, sizeof(buf), NGTCP2_PKT_INITIAL, &rcid,
       ngtcp2_conn_get_dcid(conn), ++pkt_num, conn->version, &fr);
 
-  rv = ngtcp2_conn_read_handshake(conn, &null_path, buf, pktlen, ++t);
+  rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, pktlen, ++t);
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
   CU_ASSERT(spktlen > 0);
 
@@ -2421,7 +2450,7 @@ void test_ngtcp2_conn_handshake_error(void) {
   /* client side */
   setup_handshake_client(&conn);
   conn->callbacks.recv_crypto_data = recv_crypto_handshake_error;
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
   CU_ASSERT(spktlen > 0);
 
@@ -2435,7 +2464,7 @@ void test_ngtcp2_conn_handshake_error(void) {
       conn, buf, sizeof(buf), NGTCP2_PKT_INITIAL, &conn->oscid,
       ngtcp2_conn_get_dcid(conn), ++pkt_num, conn->version, &fr);
 
-  rv = ngtcp2_conn_read_handshake(conn, &null_path, buf, pktlen, ++t);
+  rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, pktlen, ++t);
 
   CU_ASSERT(NGTCP2_ERR_CRYPTO == rv);
 
@@ -2455,95 +2484,13 @@ void test_ngtcp2_conn_handshake_error(void) {
       conn, buf, sizeof(buf), NGTCP2_PKT_INITIAL, &rcid,
       ngtcp2_conn_get_dcid(conn), ++pkt_num, conn->version, &fr);
 
-  rv = ngtcp2_conn_read_handshake(conn, &null_path, buf, pktlen, ++t);
+  rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, pktlen, ++t);
 
   CU_ASSERT(NGTCP2_ERR_CRYPTO == rv);
 
   ngtcp2_conn_del(conn);
 }
 
-void test_ngtcp2_conn_client_write_handshake(void) {
-  ngtcp2_conn *conn;
-  uint8_t buf[1240];
-  ssize_t spktlen;
-  ngtcp2_tstamp t = 0;
-  int64_t stream_id;
-  int rv;
-  ssize_t datalen;
-  ngtcp2_vec datav;
-
-  /* Verify that Handshake packet and 0-RTT packet are coalesced into
-     one UDP packet. */
-  setup_early_client(&conn);
-
-  rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
-
-  CU_ASSERT(0 == rv);
-
-  spktlen = ngtcp2_conn_client_write_handshake(conn, buf, sizeof(buf), &datalen,
-                                               stream_id, 0,
-                                               null_datav(&datav, 199), 1, ++t);
-
-  CU_ASSERT(sizeof(buf) == spktlen);
-  CU_ASSERT(199 == datalen);
-
-  ngtcp2_conn_del(conn);
-
-  /* 0 length 0-RTT packet with FIN bit set */
-  setup_early_client(&conn);
-
-  rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
-
-  CU_ASSERT(0 == rv);
-
-  spktlen = ngtcp2_conn_client_write_handshake(conn, buf, sizeof(buf), &datalen,
-                                               stream_id, 1, NULL, 0, ++t);
-
-  CU_ASSERT(sizeof(buf) == spktlen);
-  CU_ASSERT(0 == datalen);
-
-  ngtcp2_conn_del(conn);
-
-  /* Can write 0 length STREAM frame */
-  setup_early_client(&conn);
-
-  rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
-
-  CU_ASSERT(0 == rv);
-
-  spktlen = ngtcp2_conn_client_write_handshake(conn, buf, sizeof(buf), &datalen,
-                                               -1, 0, NULL, 0, ++t);
-
-  CU_ASSERT(spktlen > 0);
-
-  /* We have written Initial.  Now check that STREAM frame is
-     written. */
-  spktlen = ngtcp2_conn_client_write_handshake(conn, buf, sizeof(buf), &datalen,
-                                               stream_id, 0, NULL, 0, ++t);
-
-  CU_ASSERT(spktlen > 0);
-
-  ngtcp2_conn_del(conn);
-
-  /* Could not send 0-RTT data because buffer is too small. */
-  setup_early_client(&conn);
-
-  rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
-
-  CU_ASSERT(0 == rv);
-
-  spktlen = ngtcp2_conn_client_write_handshake(
-      conn, buf,
-      NGTCP2_MIN_LONG_HEADERLEN + 1 + ngtcp2_conn_get_dcid(conn)->datalen +
-          conn->oscid.datalen + 300,
-      &datalen, stream_id, 1, NULL, 0, ++t);
-
-  CU_ASSERT(spktlen > 0);
-  CU_ASSERT(-1 == datalen);
-
-  ngtcp2_conn_del(conn);
-}
-
 void test_ngtcp2_conn_retransmit_protected(void) {
   ngtcp2_conn *conn;
   uint8_t buf[2048];
@@ -2557,7 +2504,8 @@ void test_ngtcp2_conn_retransmit_protected(void) {
 
   ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), NULL,
-                                     stream_id, 0, null_data, 126, ++t);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 126, ++t);
 
   CU_ASSERT(spktlen > 0);
 
@@ -3359,6 +3307,7 @@ void test_ngtcp2_conn_send_early_data(void) {
   int64_t stream_id;
   int rv;
   ngtcp2_tstamp t = 0;
+  ngtcp2_vec datav;
 
   setup_early_client(&conn);
 
@@ -3366,15 +3315,12 @@ void test_ngtcp2_conn_send_early_data(void) {
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
-
-  CU_ASSERT(spktlen > 0);
-
   spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, sizeof(buf), &datalen,
-                                     stream_id, 1, null_data, 1024, ++t);
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     1, null_data, 1024, ++t);
 
   CU_ASSERT((ssize_t)sizeof(buf) == spktlen);
-  CU_ASSERT(700 == datalen);
+  CU_ASSERT(417 == datalen);
 
   ngtcp2_conn_del(conn);
 
@@ -3386,34 +3332,102 @@ void test_ngtcp2_conn_send_early_data(void) {
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, 606, &datalen,
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                     0, null_data, 10, ++t);
 
   CU_ASSERT(spktlen > 0);
+  CU_ASSERT(-1 == datalen);
+
+  ngtcp2_conn_del(conn);
+
+  /* +1 buffer size */
+  setup_early_client(&conn);
 
-  spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, 323, &datalen, stream_id,
+  rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
+
+  CU_ASSERT(0 == rv);
+
+  spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, 607, &datalen,
+                                     NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
                                      0, null_data, 10, ++t);
 
   CU_ASSERT(spktlen > 0);
-  CU_ASSERT(-1 == datalen);
+  CU_ASSERT(1 == datalen);
 
   ngtcp2_conn_del(conn);
 
-  /* +1 buffer size */
+  /* Verify that Handshake packet and 0-RTT packet are coalesced into
+     one UDP packet. */
+  setup_early_client(&conn);
+
+  rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
+
+  CU_ASSERT(0 == rv);
+
+  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, sizeof(buf), &datalen,
+                                      NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                      0, null_datav(&datav, 199), 1, ++t);
+
+  CU_ASSERT(sizeof(buf) == spktlen);
+  CU_ASSERT(199 == datalen);
+
+  ngtcp2_conn_del(conn);
+
+  /* 0 length 0-RTT packet with FIN bit set */
+  setup_early_client(&conn);
+
+  rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
+
+  CU_ASSERT(0 == rv);
+
+  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, sizeof(buf), &datalen,
+                                      NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                      1, NULL, 0, ++t);
+
+  CU_ASSERT(sizeof(buf) == spktlen);
+  CU_ASSERT(0 == datalen);
+
+  ngtcp2_conn_del(conn);
+
+  /* Can write 0 length STREAM frame */
   setup_early_client(&conn);
 
   rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, sizeof(buf), &datalen,
+                                      NGTCP2_WRITE_STREAM_FLAG_NONE, -1, 0,
+                                      NULL, 0, ++t);
 
   CU_ASSERT(spktlen > 0);
 
-  spktlen = ngtcp2_conn_write_stream(conn, NULL, buf, 324, &datalen, stream_id,
-                                     0, null_data, 10, ++t);
+  /* We have written Initial.  Now check that STREAM frame is
+     written. */
+  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, sizeof(buf), &datalen,
+                                      NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
+                                      0, NULL, 0, ++t);
 
   CU_ASSERT(spktlen > 0);
-  CU_ASSERT(1 == datalen);
+
+  ngtcp2_conn_del(conn);
+
+  /* Could not send 0-RTT data because buffer is too small. */
+  setup_early_client(&conn);
+
+  rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
+
+  CU_ASSERT(0 == rv);
+
+  spktlen = ngtcp2_conn_writev_stream(
+      conn, NULL, buf,
+      NGTCP2_MIN_LONG_HEADERLEN + 1 + ngtcp2_conn_get_dcid(conn)->datalen +
+          conn->oscid.datalen + 300,
+      &datalen, NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id, 1, NULL, 0, ++t);
+
+  CU_ASSERT(spktlen > 0);
+  CU_ASSERT(-1 == datalen);
 
   ngtcp2_conn_del(conn);
 }
@@ -3444,11 +3458,11 @@ void test_ngtcp2_conn_recv_early_data(void) {
       conn, buf, sizeof(buf), NGTCP2_PKT_INITIAL, &rcid,
       ngtcp2_conn_get_dcid(conn), ++pkt_num, conn->version, &fr);
 
-  rv = ngtcp2_conn_read_handshake(conn, &null_path, buf, pktlen, ++t);
+  rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, pktlen, ++t);
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
   CU_ASSERT(spktlen > 0);
 
@@ -3464,11 +3478,11 @@ void test_ngtcp2_conn_recv_early_data(void) {
       conn, buf, sizeof(buf), NGTCP2_PKT_0RTT, &rcid,
       ngtcp2_conn_get_dcid(conn), ++pkt_num, conn->version, &fr);
 
-  rv = ngtcp2_conn_read_handshake(conn, &null_path, buf, pktlen, ++t);
+  rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, pktlen, ++t);
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
   CU_ASSERT(spktlen > 0);
 
@@ -3494,11 +3508,11 @@ void test_ngtcp2_conn_recv_early_data(void) {
       conn, buf, sizeof(buf), NGTCP2_PKT_0RTT, &rcid,
       ngtcp2_conn_get_dcid(conn), ++pkt_num, conn->version, &fr);
 
-  rv = ngtcp2_conn_read_handshake(conn, &null_path, buf, pktlen, ++t);
+  rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, pktlen, ++t);
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
   CU_ASSERT(0 == spktlen);
 
@@ -3512,11 +3526,11 @@ void test_ngtcp2_conn_recv_early_data(void) {
       conn, buf, sizeof(buf), NGTCP2_PKT_INITIAL, &rcid,
       ngtcp2_conn_get_dcid(conn), ++pkt_num, conn->version, &fr);
 
-  rv = ngtcp2_conn_read_handshake(conn, &null_path, buf, pktlen, ++t);
+  rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, pktlen, ++t);
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
   CU_ASSERT(spktlen > 0);
 
@@ -3552,11 +3566,11 @@ void test_ngtcp2_conn_recv_early_data(void) {
       conn, buf + pktlen, sizeof(buf) - pktlen, NGTCP2_PKT_0RTT, &rcid,
       ngtcp2_conn_get_dcid(conn), ++pkt_num, conn->version, &fr);
 
-  rv = ngtcp2_conn_read_handshake(conn, &null_path, buf, pktlen, ++t);
+  rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, pktlen, ++t);
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
   CU_ASSERT(spktlen > 0);
 
@@ -3597,11 +3611,11 @@ void test_ngtcp2_conn_recv_compound_pkt(void) {
       conn, buf + pktlen, sizeof(buf) - pktlen, NGTCP2_PKT_INITIAL,
       &conn->oscid, ngtcp2_conn_get_dcid(conn), ++pkt_num, conn->version, &fr);
 
-  rv = ngtcp2_conn_read_handshake(conn, &null_path, buf, pktlen, ++t);
+  rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, pktlen, ++t);
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
   CU_ASSERT(spktlen > 0);
 
@@ -3687,12 +3701,12 @@ void test_ngtcp2_conn_pkt_payloadlen(void) {
   write_pkt_payloadlen(buf, dcid, &conn->oscid, payloadlen + 1);
 
   /* The incoming packet should be ignored */
-  rv = ngtcp2_conn_read_handshake(conn, &null_path, buf, pktlen, ++t);
+  rv = ngtcp2_conn_read_pkt(conn, &null_path, buf, pktlen, ++t);
 
   CU_ASSERT(0 == rv);
   CU_ASSERT(NGTCP2_CS_SERVER_INITIAL == conn->state);
 
-  spktlen = ngtcp2_conn_write_handshake(conn, buf, sizeof(buf), ++t);
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
 
   CU_ASSERT(spktlen == 0);
   CU_ASSERT(0 == ngtcp2_ksl_len(&conn->in_pktns.acktr.ents));
@@ -3709,6 +3723,7 @@ void test_ngtcp2_conn_writev_stream(void) {
   int64_t stream_id;
   ngtcp2_vec datav = {null_data, 10};
   ssize_t datalen;
+  size_t left;
 
   /* 0 length STREAM should not be written if we supply nonzero length
      data. */
@@ -3728,7 +3743,8 @@ void test_ngtcp2_conn_writev_stream(void) {
    * STREAM overhead (+3)
    * AEAD overhead (16)
    */
-  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, 39, &datalen, stream_id,
+  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, 39, &datalen,
+                                      NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
                                       0, &datav, 1, ++t);
 
   CU_ASSERT(0 == spktlen);
@@ -3748,13 +3764,85 @@ void test_ngtcp2_conn_writev_stream(void) {
 
   CU_ASSERT(0 == rv);
 
-  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, 40, &datalen, stream_id,
+  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, 40, &datalen,
+                                      NGTCP2_WRITE_STREAM_FLAG_NONE, stream_id,
                                       0, &datav, 1, ++t);
 
   CU_ASSERT(spktlen > 0);
   CU_ASSERT(1 == datalen);
 
   ngtcp2_conn_del(conn);
+
+  /* Coalesces multiple STREAM frames */
+  setup_default_client(&conn);
+  conn->local.bidi.max_streams = 100;
+
+  rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
+
+  CU_ASSERT(0 == rv);
+
+  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, 1200, &datalen,
+                                      NGTCP2_WRITE_STREAM_FLAG_MORE, stream_id,
+                                      0, &datav, 1, ++t);
+
+  CU_ASSERT(NGTCP2_ERR_WRITE_STREAM_MORE == spktlen);
+  CU_ASSERT(10 == datalen);
+
+  left = ngtcp2_ppe_left(&conn->pkt.ppe);
+
+  rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
+
+  CU_ASSERT(0 == rv);
+
+  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, 1200, &datalen,
+                                      NGTCP2_WRITE_STREAM_FLAG_MORE, stream_id,
+                                      0, &datav, 1, ++t);
+
+  CU_ASSERT(NGTCP2_ERR_WRITE_STREAM_MORE == spktlen);
+  CU_ASSERT(10 == datalen);
+  CU_ASSERT(ngtcp2_ppe_left(&conn->pkt.ppe) < left);
+
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
+
+  CU_ASSERT(spktlen > 0);
+
+  ngtcp2_conn_del(conn);
+
+  /* 0RTT: Coalesces multiple STREAM frames */
+  setup_early_client(&conn);
+  conn->local.bidi.max_streams = 100;
+
+  rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
+
+  CU_ASSERT(0 == rv);
+
+  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, 1200, &datalen,
+                                      NGTCP2_WRITE_STREAM_FLAG_MORE, stream_id,
+                                      0, &datav, 1, ++t);
+
+  CU_ASSERT(NGTCP2_ERR_WRITE_STREAM_MORE == spktlen);
+  CU_ASSERT(10 == datalen);
+
+  left = ngtcp2_ppe_left(&conn->pkt.ppe);
+
+  rv = ngtcp2_conn_open_bidi_stream(conn, &stream_id, NULL);
+
+  CU_ASSERT(0 == rv);
+
+  spktlen = ngtcp2_conn_writev_stream(conn, NULL, buf, 1200, &datalen,
+                                      NGTCP2_WRITE_STREAM_FLAG_MORE, stream_id,
+                                      0, &datav, 1, ++t);
+
+  CU_ASSERT(NGTCP2_ERR_WRITE_STREAM_MORE == spktlen);
+  CU_ASSERT(10 == datalen);
+  CU_ASSERT(ngtcp2_ppe_left(&conn->pkt.ppe) < left);
+
+  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), ++t);
+
+  /* Make sure that packet is padded */
+  CU_ASSERT(1200 == spktlen);
+
+  ngtcp2_conn_del(conn);
 }
 
 void test_ngtcp2_conn_recv_new_connection_id(void) {
@@ -3906,7 +3994,6 @@ void test_ngtcp2_conn_client_connection_migration(void) {
   ngtcp2_conn *conn;
   uint8_t buf[2048];
   size_t pktlen;
-  ssize_t spktlen;
   ngtcp2_tstamp t = 900;
   int64_t pkt_num = 0;
   ngtcp2_frame fr;
@@ -3934,22 +4021,7 @@ void test_ngtcp2_conn_client_connection_migration(void) {
   rv = ngtcp2_conn_initiate_migration(conn, &new_path, ++t);
 
   CU_ASSERT(0 == rv);
-  CU_ASSERT(NULL != conn->pv);
-
-  spktlen = ngtcp2_conn_write_pkt(conn, NULL, buf, sizeof(buf), t);
-
-  CU_ASSERT(spktlen > 0);
-  CU_ASSERT(ngtcp2_ringbuf_len(&conn->pv->ents) > 0);
-
-  fr.type = NGTCP2_FRAME_PATH_RESPONSE;
-  memset(fr.path_response.data, 0, sizeof(fr.path_response.data));
-
-  pktlen = write_single_frame_pkt(conn, buf, sizeof(buf), &conn->oscid,
-                                  ++pkt_num, &fr);
-
-  rv = ngtcp2_conn_read_pkt(conn, &new_path, buf, pktlen, ++t);
-
-  CU_ASSERT(0 == rv);
+  CU_ASSERT(NULL == conn->pv);
   CU_ASSERT(ngtcp2_path_eq(&new_path, &conn->dcid.current.ps.path));
   CU_ASSERT(ngtcp2_cid_eq(&cid, &conn->dcid.current.cid));
 
diff --git a/tests/ngtcp2_conn_test.h b/tests/ngtcp2_conn_test.h
index 6ddd4b2ac8013ad27d0c1c47c7a28d64384939fa..5cbf9aad49c5f2e66136ae2a6b8074f226f512ac 100644
--- a/tests/ngtcp2_conn_test.h
+++ b/tests/ngtcp2_conn_test.h
@@ -47,7 +47,6 @@ void test_ngtcp2_conn_recv_delayed_handshake_pkt(void);
 void test_ngtcp2_conn_recv_max_streams(void);
 void test_ngtcp2_conn_handshake(void);
 void test_ngtcp2_conn_handshake_error(void);
-void test_ngtcp2_conn_client_write_handshake(void);
 void test_ngtcp2_conn_retransmit_protected(void);
 void test_ngtcp2_conn_send_max_stream_data(void);
 void test_ngtcp2_conn_recv_stream_data(void);