diff --git a/buffer-builder.cabal b/buffer-builder.cabal
--- a/buffer-builder.cabal
+++ b/buffer-builder.cabal
@@ -1,5 +1,5 @@
 name:                buffer-builder
-version:             0.2.4.2
+version:             0.2.4.3
 synopsis:            Library for efficiently building up buffers, one piece at a time
 description:
 
diff --git a/cbits/buffer.cpp b/cbits/buffer.cpp
--- a/cbits/buffer.cpp
+++ b/cbits/buffer.cpp
@@ -37,6 +37,10 @@
 
 
 namespace {
+    static const unsigned char DEC2HEX[16] = {
+        '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'
+    };
+
     struct BufferWriter {
         unsigned char* end; // the most commonly-accessed field
         unsigned char* capacity;
@@ -269,14 +273,20 @@
             case '\t': rv += 2; break;
             case '\\': rv += 2; break;
             case '\"': rv += 2; break;
-            default:   rv += 1; break;
+            default:
+                if (data[i] <= 0x1F) {
+                    rv += 6;
+                } else {
+                    rv += 1;
+                }
+                break;
         }
     }
     return rv;
 }
 
 extern "C" void bw_append_json_escaped(BufferWriter* bw, size_t size, const unsigned char* data) {
-    BW_ENSURE_CAPACITY(bw, 2 + size * 2, count_escaped_json_length(size, data));
+    BW_ENSURE_CAPACITY(bw, 2 + size * 6, count_escaped_json_length(size, data));
 
     unsigned char* dest = bw->end;
     *dest++ = '\"';
@@ -289,7 +299,19 @@
             case '\t': *dest++ = '\\'; *dest++ = 't';  break;
             case '\\': *dest++ = '\\'; *dest++ = '\\'; break;
             case '\"': *dest++ = '\\'; *dest++ = '\"'; break;
-            default:   *dest++ = data[i];              break;
+            default:
+                unsigned char d = data[i];
+                if (d <= 0x1F) {
+                    *dest++ = '\\';
+                    *dest++ = 'u';
+                    *dest++ = '0';
+                    *dest++ = '0';
+                    *dest++ = DEC2HEX[d >> 4];
+                    *dest++ = DEC2HEX[d & 0xF];
+                } else {
+                    *dest++ = d;
+                }
+                break;
         }
         ++i;
     }
@@ -434,10 +456,6 @@
     0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
     0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
     0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0
-};
-
-static const unsigned char DEC2HEX[16] = {
-    '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'
 };
 
 inline size_t count_url_encoded_length(size_t size, const unsigned char* data) {
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,7 @@
+0.2.4.3
+
+* Fix a bug where control characters (codepoints 1 through 31) were not being escaped.
+
 0.2.4.2
 
 * Include test.json in sdist so benchmarks can be run from Hackage.  (Thanks Ryan Scott!)
diff --git a/test/JsonTest.hs b/test/JsonTest.hs
--- a/test/JsonTest.hs
+++ b/test/JsonTest.hs
@@ -38,6 +38,8 @@
 
     ae "\"\\\"\\\\\\n\\r\\t\"" (encodeJson (fromString ['\"', '\\', '\n', '\r', '\t'] :: Text))
 
+    ae "\"\\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\u0008\\t\\n\\u000b\\u000c\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\"" (encodeJson (fromString $ map toEnum [0..31] :: Text))
+
 test_encode_object :: IO ()
 test_encode_object = do
     ae "{\"key\":\"value\"}" (encodeJson ("key" .= ("value" :: Text)))
