diff --git a/configure.ac b/configure.ac
index 5bcfc6d79f28df7163bd6451672001887ed6afbd..89727e648a3ca3234d0a135d86546bd224375b57 100644
--- a/configure.ac
+++ b/configure.ac
@@ -96,7 +96,7 @@ AC_PROG_MKDIR_P
 
 PKG_PROG_PKG_CONFIG([0.20])
 
-AX_CXX_COMPILE_STDCXX([14], [noext], [optional])
+AX_CXX_COMPILE_STDCXX([17], [noext], [optional])
 
 # Checks for libraries.
 
diff --git a/examples/examplestest.cc b/examples/examplestest.cc
index 9e61689d087177099ee6136005ca69ec3e5313f5..2c35f7a776f252fa06497e8ec30fa9f9e05e994f 100644
--- a/examples/examplestest.cc
+++ b/examples/examplestest.cc
@@ -53,8 +53,18 @@ int main(int argc, char *argv[]) {
   }
 
   // add the tests to the suite
-  if (!CU_add_test(pSuite, "util_format_duration",
-                   ngtcp2::test_util_format_duration)) {
+  if (!CU_add_test(pSuite, "util_format_durationf",
+                   ngtcp2::test_util_format_durationf) ||
+      !CU_add_test(pSuite, "util_format_uint", ngtcp2::test_util_format_uint) ||
+      !CU_add_test(pSuite, "util_format_uint_iec",
+                   ngtcp2::test_util_format_uint_iec) ||
+      !CU_add_test(pSuite, "util_format_duration",
+                   ngtcp2::test_util_format_duration) ||
+      !CU_add_test(pSuite, "util_parse_uint", ngtcp2::test_util_parse_uint) ||
+      !CU_add_test(pSuite, "util_parse_uint_iec",
+                   ngtcp2::test_util_parse_uint_iec) ||
+      !CU_add_test(pSuite, "util_parse_duration",
+                   ngtcp2::test_util_parse_duration)) {
     CU_cleanup_registry();
     return CU_get_error();
   }
diff --git a/examples/util.cc b/examples/util.cc
index 3b4635864deb4074dd78b72d958186bcdaa7f587..4cd37b69b9690f54c7cd4dd77b7aed0aaae2b191 100644
--- a/examples/util.cc
+++ b/examples/util.cc
@@ -39,6 +39,7 @@
 #include <iostream>
 #include <fstream>
 #include <algorithm>
+#include <limits>
 
 namespace ngtcp2 {
 
@@ -131,7 +132,7 @@ uint64_t round2even(uint64_t n) {
 }
 } // namespace
 
-std::string format_duration(uint64_t ns) {
+std::string format_durationf(uint64_t ns) {
   static constexpr const char *units[] = {"us", "ms", "s"};
   if (ns < 1000) {
     return std::to_string(ns) + "ns";
@@ -334,6 +335,132 @@ OSSL_ENCRYPTION_LEVEL from_ngtcp2_level(ngtcp2_crypto_level crypto_level) {
   }
 }
 
+namespace {
+std::tuple<uint64_t, size_t, int> parse_uint_internal(const std::string &s) {
+  uint64_t res = 0;
+
+  if (s.empty()) {
+    return {0, 0, -1};
+  }
+
+  for (size_t i = 0; i < s.size(); ++i) {
+    auto c = s[i];
+    if (c < '0' || '9' < c) {
+      return {res, i, 0};
+    }
+
+    auto d = c - '0';
+    if (res > (std::numeric_limits<uint64_t>::max() - d) / 10) {
+      return {0, i, -1};
+    }
+
+    res *= 10;
+    res += d;
+  }
+
+  return {res, s.size(), 0};
+}
+} // namespace
+
+std::pair<uint64_t, int> parse_uint(const std::string &s) {
+  auto [res, idx, rv] = parse_uint_internal(s);
+  if (rv != 0 || idx != s.size()) {
+    return {0, -1};
+  }
+  return {res, 0};
+}
+
+std::pair<uint64_t, int> parse_uint_iec(const std::string &s) {
+  auto [res, idx, rv] = parse_uint_internal(s);
+  if (rv != 0) {
+    return {0, rv};
+  }
+  if (idx == s.size()) {
+    return {res, 0};
+  }
+  if (idx + 1 != s.size()) {
+    return {0, -1};
+  }
+
+  uint64_t m;
+  switch (s[idx]) {
+  case 'G':
+  case 'g':
+    m = 1 << 30;
+    break;
+  case 'M':
+  case 'm':
+    m = 1 << 20;
+    break;
+  case 'K':
+  case 'k':
+    m = 1 << 10;
+    break;
+  default:
+    return {0, -1};
+  }
+
+  if (res > std::numeric_limits<uint64_t>::max() / m) {
+    return {0, -1};
+  }
+
+  return {res * m, 0};
+}
+
+std::pair<uint64_t, int> parse_duration(const std::string &s) {
+  auto [res, idx, rv] = parse_uint_internal(s);
+  if (rv != 0) {
+    return {0, rv};
+  }
+  if (idx == s.size()) {
+    return {res, 0};
+  }
+
+  uint64_t m;
+  if (idx + 1 == s.size()) {
+    switch (s[idx]) {
+    case 'H':
+    case 'h':
+      m = 3600 * NGTCP2_SECONDS;
+      break;
+    case 'M':
+    case 'm':
+      m = 60 * NGTCP2_SECONDS;
+      break;
+    case 'S':
+    case 's':
+      m = NGTCP2_SECONDS;
+      break;
+    default:
+      return {0, -1};
+    }
+  } else if (idx + 2 == s.size() && (s[idx + 1] == 's' || s[idx + 1] == 'S')) {
+    switch (s[idx]) {
+    case 'M':
+    case 'm':
+      m = NGTCP2_MILLISECONDS;
+      break;
+    case 'U':
+    case 'u':
+      m = NGTCP2_MICROSECONDS;
+      break;
+    case 'N':
+    case 'n':
+      return {res, 0};
+    default:
+      return {0, -1};
+    }
+  } else {
+    return {0, -1};
+  }
+
+  if (res > std::numeric_limits<uint64_t>::max() / m) {
+    return {0, -1};
+  }
+
+  return {res * m, 0};
+}
+
 } // namespace util
 
 } // namespace ngtcp2
diff --git a/examples/util.h b/examples/util.h
index 10ef898b5920de2561b75284ffe6d50761bc10ab..74ddfbada4fe573c03f5be02afcd8612c5040597 100644
--- a/examples/util.h
+++ b/examples/util.h
@@ -77,12 +77,12 @@ template <size_t N> std::string format_hex(const uint8_t (&s)[N]) {
 
 std::string decode_hex(const std::string &s);
 
-// format_duration formats |ns| in human readable manner.  |ns| must
+// format_durationf formats |ns| in human readable manner.  |ns| must
 // be nanoseconds resolution.  This function uses the largest unit so
 // that the integral part is strictly more than zero, and the
 // precision is at most 2 digits.  For example, 1234 is formatted as
 // "1.23us".  The largest unit is seconds.
-std::string format_duration(uint64_t ns);
+std::string format_durationf(uint64_t ns);
 
 std::mt19937 make_mt19937();
 
@@ -215,6 +215,77 @@ ngtcp2_crypto_level from_ossl_level(OSSL_ENCRYPTION_LEVEL ossl_level);
 // OSSL_ENCRYPTION_LEVEL.
 OSSL_ENCRYPTION_LEVEL from_ngtcp2_level(ngtcp2_crypto_level crypto_level);
 
+// format_uint converts |n| into string.
+template <typename T> std::string format_uint(T n) {
+  std::string res;
+  if (n == 0) {
+    res = "0";
+    return res;
+  }
+  size_t nlen = 0;
+  for (auto t = n; t; t /= 10, ++nlen)
+    ;
+  res.resize(nlen);
+  for (; n; n /= 10) {
+    res[--nlen] = (n % 10) + '0';
+  }
+  return res;
+}
+
+// format_uint_iec converts |n| into string with the IEC unit (either
+// "G", "M", or "K").  It chooses the largest unit which does not drop
+// precision.
+template <typename T> std::string format_uint_iec(T n) {
+  if (n >= (1 << 30) && (n & ((1 << 30) - 1)) == 0) {
+    return format_uint(n / (1 << 30)) + 'G';
+  }
+  if (n >= (1 << 20) && (n & ((1 << 20) - 1)) == 0) {
+    return format_uint(n / (1 << 20)) + 'M';
+  }
+  if (n >= (1 << 10) && (n & ((1 << 10) - 1)) == 0) {
+    return format_uint(n / (1 << 10)) + 'K';
+  }
+  return format_uint(n);
+}
+
+// format_duration converts |n| into string with the unit in either
+// "h" (hours), "m" (minutes), "s" (seconds), "ms" (milliseconds),
+// "us" (microseconds) or "ns" (nanoseconds).  It chooses the largest
+// unit which does not drop precision.  |n| is in nanosecond
+// resolution.
+template <typename T> std::string format_duration(T n) {
+  if (n >= 3600 * NGTCP2_SECONDS && (n % (3600 * NGTCP2_SECONDS)) == 0) {
+    return format_uint(n / (3600 * NGTCP2_SECONDS)) + 'h';
+  }
+  if (n >= 60 * NGTCP2_SECONDS && (n % (60 * NGTCP2_SECONDS)) == 0) {
+    return format_uint(n / (60 * NGTCP2_SECONDS)) + 'm';
+  }
+  if (n >= NGTCP2_SECONDS && (n % NGTCP2_SECONDS) == 0) {
+    return format_uint(n / NGTCP2_SECONDS) + 's';
+  }
+  if (n >= NGTCP2_MILLISECONDS && (n % NGTCP2_MILLISECONDS) == 0) {
+    return format_uint(n / NGTCP2_MILLISECONDS) + "ms";
+  }
+  if (n >= NGTCP2_MICROSECONDS && (n % NGTCP2_MICROSECONDS) == 0) {
+    return format_uint(n / NGTCP2_MICROSECONDS) + "us";
+  }
+  return format_uint(n) + "ns";
+}
+
+// parse_uint parses |s| as 64-bit unsigned integer.  If it cannot
+// parse |s|, it returns -1 as the second return value.
+std::pair<uint64_t, int> parse_uint(const std::string &s);
+
+// parse_uint_iec parses |s| as 64-bit unsigned integer.  It accepts
+// IEC unit letter (either "G", "M", or "K") in |s|.  If it cannot
+// parse |s|, it returns -1 as the second return value.
+std::pair<uint64_t, int> parse_uint_iec(const std::string &s);
+
+// parse_duration parses |s| as 64-bit unsigned integer.  It accepts a
+// unit (either "h", "m", "s", "ms", "us", or "ns") in |s|.  If it
+// cannot parse |s|, it returns -1 as the second return value.
+std::pair<uint64_t, int> parse_duration(const std::string &s);
+
 } // namespace util
 
 } // namespace ngtcp2
diff --git a/examples/util_test.cc b/examples/util_test.cc
index 9206df7bd6d62ce50568a5f61a1bac7414118f97..d9bc00c8e53fa45c5b6f633ef0ead335aad194e4 100644
--- a/examples/util_test.cc
+++ b/examples/util_test.cc
@@ -24,23 +24,194 @@
  */
 #include "util_test.h"
 
+#include <limits>
+
 #include <CUnit/CUnit.h>
 
 #include "util.h"
 
 namespace ngtcp2 {
 
+void test_util_format_durationf() {
+  CU_ASSERT("0ns" == util::format_durationf(0));
+  CU_ASSERT("999ns" == util::format_durationf(999));
+  CU_ASSERT("1.00us" == util::format_durationf(1000));
+  CU_ASSERT("1.00us" == util::format_durationf(1004));
+  CU_ASSERT("1.00us" == util::format_durationf(1005));
+  CU_ASSERT("1.02us" == util::format_durationf(1015));
+  CU_ASSERT("2.00us" == util::format_durationf(1999));
+  CU_ASSERT("1.00ms" == util::format_durationf(999999));
+  CU_ASSERT("3.50ms" == util::format_durationf(3500111));
+  CU_ASSERT("9999.99s" == util::format_durationf(9999990000000llu));
+}
+
+void test_util_format_uint() {
+  CU_ASSERT("0" == util::format_uint(0));
+  CU_ASSERT("18446744073709551615" ==
+            util::format_uint(18446744073709551615ull));
+}
+
+void test_util_format_uint_iec() {
+  CU_ASSERT("0" == util::format_uint_iec(0));
+  CU_ASSERT("1023" == util::format_uint_iec((1 << 10) - 1));
+  CU_ASSERT("1K" == util::format_uint_iec(1 << 10));
+  CU_ASSERT("1M" == util::format_uint_iec(1 << 20));
+  CU_ASSERT("1G" == util::format_uint_iec(1 << 30));
+  CU_ASSERT("18446744073709551615" ==
+            util::format_uint_iec(std::numeric_limits<uint64_t>::max()));
+  CU_ASSERT("1025K" == util::format_uint_iec((1 << 20) + (1 << 10)));
+}
+
 void test_util_format_duration() {
   CU_ASSERT("0ns" == util::format_duration(0));
   CU_ASSERT("999ns" == util::format_duration(999));
-  CU_ASSERT("1.00us" == util::format_duration(1000));
-  CU_ASSERT("1.00us" == util::format_duration(1004));
-  CU_ASSERT("1.00us" == util::format_duration(1005));
-  CU_ASSERT("1.02us" == util::format_duration(1015));
-  CU_ASSERT("2.00us" == util::format_duration(1999));
-  CU_ASSERT("1.00ms" == util::format_duration(999999));
-  CU_ASSERT("3.50ms" == util::format_duration(3500111));
-  CU_ASSERT("9999.99s" == util::format_duration(9999990000000llu));
+  CU_ASSERT("1us" == util::format_duration(1000));
+  CU_ASSERT("1ms" == util::format_duration(1000000));
+  CU_ASSERT("1s" == util::format_duration(1000000000));
+  CU_ASSERT("1m" == util::format_duration(60000000000ull));
+  CU_ASSERT("1h" == util::format_duration(3600000000000ull));
+  CU_ASSERT("18446744073709551615ns" ==
+            util::format_duration(std::numeric_limits<uint64_t>::max()));
+  CU_ASSERT("61s" == util::format_duration(61000000000ull));
+}
+
+void test_util_parse_uint() {
+  {
+    auto [res, rv] = util::parse_uint("0");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(0 == res);
+  }
+  {
+    auto [res, rv] = util::parse_uint("1");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(1 == res);
+  }
+  {
+    auto [res, rv] = util::parse_uint("18446744073709551615");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(18446744073709551615ull == res);
+  }
+  {
+    auto [_, rv] = util::parse_uint("18446744073709551616");
+    CU_ASSERT(-1 == rv);
+  }
+  {
+    auto [_, rv] = util::parse_uint("a");
+    CU_ASSERT(-1 == rv);
+  }
+  {
+    auto [_, rv] = util::parse_uint("1a");
+    CU_ASSERT(-1 == rv);
+  }
+}
+
+void test_util_parse_uint_iec() {
+  {
+    auto [res, rv] = util::parse_uint_iec("0");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(0 == res);
+  }
+  {
+    auto [res, rv] = util::parse_uint_iec("1023");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(1023 == res);
+  }
+  {
+    auto [res, rv] = util::parse_uint_iec("1K");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(1 << 10 == res);
+  }
+  {
+    auto [res, rv] = util::parse_uint_iec("1M");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(1 << 20 == res);
+  }
+  {
+    auto [res, rv] = util::parse_uint_iec("1G");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(1 << 30 == res);
+  }
+  {
+    auto [res, rv] = util::parse_uint_iec("11G");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT((1ull << 30) * 11);
+  }
+  {
+    auto [_, rv] = util::parse_uint_iec("18446744073709551616");
+    CU_ASSERT(-1 == rv);
+  }
+  {
+    auto [_, rv] = util::parse_uint_iec("1x");
+    CU_ASSERT(-1 == rv);
+  }
+  {
+    auto [_, rv] = util::parse_uint_iec("1Gx");
+    CU_ASSERT(-1 == rv);
+  }
+}
+
+void test_util_parse_duration() {
+  {
+    auto [res, rv] = util::parse_duration("0");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(0 == res);
+  }
+  {
+    auto [res, rv] = util::parse_duration("0ns");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(0 == res);
+  }
+  {
+    auto [res, rv] = util::parse_duration("1ns");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(1 == res);
+  }
+  {
+    auto [res, rv] = util::parse_duration("1us");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(NGTCP2_MICROSECONDS == res);
+  }
+  {
+    auto [res, rv] = util::parse_duration("1ms");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(NGTCP2_MILLISECONDS == res);
+  }
+  {
+    auto [res, rv] = util::parse_duration("1s");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(NGTCP2_SECONDS == res);
+  }
+  {
+    auto [res, rv] = util::parse_duration("1m");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(60 * NGTCP2_SECONDS == res);
+  }
+  {
+    auto [res, rv] = util::parse_duration("1h");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(3600 * NGTCP2_SECONDS == res);
+  }
+  {
+    auto [res, rv] = util::parse_duration("2h");
+    CU_ASSERT(0 == rv);
+    CU_ASSERT(2 * 3600 * NGTCP2_SECONDS == res);
+  }
+  {
+    auto [_, rv] = util::parse_duration("18446744073709551616");
+    CU_ASSERT(-1 == rv);
+  }
+  {
+    auto [_, rv] = util::parse_duration("1x");
+    CU_ASSERT(-1 == rv);
+  }
+  {
+    auto [_, rv] = util::parse_duration("1mx");
+    CU_ASSERT(-1 == rv);
+  }
+  {
+    auto [_, rv] = util::parse_duration("1mxy");
+    CU_ASSERT(-1 == rv);
+  }
 }
 
 } // namespace ngtcp2
diff --git a/examples/util_test.h b/examples/util_test.h
index 20eb1200d748ca561045fb9e952969b0592f46e2..06a0a84a7ebc6b907c87255fc787ccf74a4927f9 100644
--- a/examples/util_test.h
+++ b/examples/util_test.h
@@ -31,7 +31,13 @@
 
 namespace ngtcp2 {
 
+void test_util_format_durationf();
+void test_util_format_uint();
+void test_util_format_uint_iec();
 void test_util_format_duration();
+void test_util_parse_uint();
+void test_util_parse_uint_iec();
+void test_util_parse_duration();
 
 } // namespace ngtcp2