diff --git a/Data/BufferBuilder.hs b/Data/BufferBuilder.hs
new file mode 100644
--- /dev/null
+++ b/Data/BufferBuilder.hs
@@ -0,0 +1,89 @@
+{-# LANGUAGE OverloadedStrings, MagicHash, UnboxedTuples, BangPatterns, GeneralizedNewtypeDeriving #-}
+
+module Data.BufferBuilder
+    ( BufferBuilder
+    , runBufferBuilder
+    , appendByte
+    , appendChar8
+    , appendBS
+    ) where
+
+import GHC.Base
+import GHC.Word
+import GHC.Ptr
+import GHC.IO
+import GHC.ForeignPtr
+import Foreign.ForeignPtr
+import Foreign.Marshal.Alloc
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Internal as BS
+import Control.Monad.Reader
+
+data BWHandle'
+type BWHandle = Ptr BWHandle'
+
+foreign import ccall unsafe "bw_new" bw_new :: Int -> IO BWHandle
+foreign import ccall unsafe "&bw_free" bw_free :: FunPtr (BWHandle -> IO ())
+foreign import ccall unsafe "bw_append_byte" bw_append_byte :: BWHandle -> Word8 -> IO ()
+foreign import ccall unsafe "bw_append_bs" bw_append_bs :: BWHandle -> Int -> (Ptr Word8) -> IO ()
+foreign import ccall unsafe "bw_get_size" bw_get_size :: BWHandle -> IO Int
+foreign import ccall unsafe "bw_trim_and_release_address" bw_trim_and_release_address :: BWHandle -> IO (Ptr Word8)
+
+-- | BufferBuilder sequences actions that append to an implicit,
+-- growable buffer.  Use 'runBufferBuilder' to extract the resulting
+-- buffer as a 'BS.ByteString'
+newtype BufferBuilder a = BB (ReaderT BWHandle IO a)
+    deriving (Functor, Monad, MonadReader BWHandle)
+
+inBW :: IO a -> BufferBuilder a
+inBW = BB . lift
+
+initialCapacity :: Int
+initialCapacity = 48
+-- why 48? it's only 6 64-bit words...  yet many small strings should fit.
+-- some quantitative analysis would be good.
+-- an option to set the initial capacity would be better. :)
+
+-- | Runs a BufferBuilder and extracts its resulting contents as a 'BS.ByteString'
+runBufferBuilder :: BufferBuilder () -> BS.ByteString
+runBufferBuilder = unsafeDupablePerformIO . runBufferBuilderIO initialCapacity
+
+runBufferBuilderIO :: Int -> BufferBuilder () -> IO BS.ByteString
+runBufferBuilderIO !capacity !(BB bw) = do
+    handle <- bw_new capacity
+    handleFP <- newForeignPtr bw_free handle
+    () <- runReaderT bw handle
+    size <- bw_get_size handle
+    src <- bw_trim_and_release_address handle
+
+    borrowed <- newForeignPtr finalizerFree src
+    let bs = BS.fromForeignPtr borrowed 0 size
+    touchForeignPtr handleFP
+    return bs
+
+
+appendByte :: Word8 -- ^ byte to append to the buffer.
+           -> BufferBuilder ()
+appendByte b = do
+    h <- ask
+    inBW $ bw_append_byte h b
+{-# INLINE appendByte #-}
+
+c2w :: Char -> Word8
+c2w = fromIntegral . ord
+{-# INLINE c2w #-}
+
+-- | Appends a character to the buffer, truncating it to the bottom 8 bits.
+appendChar8 :: Char -- ^ character to append to the buffer
+            -> BufferBuilder ()
+appendChar8 = appendByte . c2w
+{-# INLINE appendChar8 #-}
+
+-- | Appends a ByteString to the buffer.
+appendBS :: BS.ByteString -- ^ 'BS.ByteString' to append
+         -> BufferBuilder ()
+appendBS !(BS.PS (ForeignPtr addr _) offset len) = do
+    h <- ask
+    inBW $ bw_append_bs h len (plusPtr (Ptr addr) offset)
+{-# INLINE appendBS #-}
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2015, IMVU, Chad Austin
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Chad Austin nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/bench/Bench.hs b/bench/Bench.hs
new file mode 100644
--- /dev/null
+++ b/bench/Bench.hs
@@ -0,0 +1,77 @@
+{-# LANGUAGE OverloadedStrings, BangPatterns, RecordWildCards #-}
+
+import Data.ByteString (ByteString)
+import Criterion.Main
+import Data.BufferBuilder
+import Control.Monad
+
+scheme :: ByteString
+scheme = "http"
+
+host :: ByteString
+host = "example.com"
+
+path :: ByteString
+path = "the/path/goes/here"
+
+buildURL :: Int -> ByteString
+buildURL times = runBufferBuilder $ do
+    replicateM_ times $ do
+        -- Sadly, if you look at the generated code, there are many
+        -- continuations (thus indirect jumps, thus inefficient stack
+        -- traffic) here.  Even though the test strings are constant,
+        -- they become ByteString CAFs, and are tag-checked on every
+        -- use.  However, appendBS followed by appendChar8 are folded
+        -- into the same generated function.
+        appendBS scheme
+        appendBS "://"
+        appendBS host
+        appendChar8 '/'
+        appendBS path
+        appendChar8 '?'
+        appendBS "key"
+        appendChar8 '='
+        appendBS "value"
+        appendChar8 '?'
+        appendBS "otherkey"
+        appendChar8 '='
+        appendBS "othervalue"
+        appendChar8 '#'
+        appendBS "hashyhashyhashy"
+
+data Record = Record
+              { f1 :: !ByteString
+              , f2 :: !ByteString
+              , f3 :: !ByteString
+              , f4 :: !ByteString
+              , f5 :: !ByteString
+              , f6 :: !ByteString
+              }
+
+encodeType :: Record -> ByteString
+encodeType !(Record{..}) = runBufferBuilder $ do
+    -- Because the record elements are strict, all of these appendBS
+    -- calls are emitted in one long Cmm/x86 function.  Can't do much
+    -- better than that.  :) However, if you look closely, the
+    -- BufferWriter handle (accessed through ReaderT) is reloaded from
+    -- the stack after all appendBS.  In the future, GHC could realize
+    -- it's always the same value and only load it once.
+    appendBS f1
+    appendBS f2
+    appendBS f3
+    appendBS f4
+    appendBS f5
+    appendBS f6
+
+recordValue :: Record
+recordValue = Record { f1 = "the wheels on the bus go round and round"
+                     , f2 = "round and round, round and round"
+                     , f3 = "the seats on the bus go up and down"
+                     , f4 = "up and down, up and down"
+                     , f5 = "the doors on the bus go open and shut"
+                     , f6 = "open and shut, open and shut"
+                     }
+
+main :: IO ()
+main = defaultMain [ bench "buildURL" $ nf buildURL 10
+                   , bench "encodeRecord" $ nf encodeType recordValue ]
diff --git a/buffer-builder.cabal b/buffer-builder.cabal
new file mode 100644
--- /dev/null
+++ b/buffer-builder.cabal
@@ -0,0 +1,87 @@
+name:                buffer-builder
+version:             0.1.0.0
+synopsis:            Library for efficiently building up buffers, one piece at a time
+description:
+
+    'BufferBuilder' is an efficient library for incrementally building
+    up 'ByteString's, one chunk at a time.  Early benchmarks show it
+    is over twice as fast as ByteString Builder, primarily because
+    'BufferBuilder' is built upon an ST-style restricted monad and
+    mutable state instead of ByteString Builder's monoidal AST.
+    .
+    Internally, BufferBuilder is backed by a few C functions.
+    Examination of GHC's output shows nearly optimal code generation
+    with no intermediate thunks -- and thus, continuation passing and
+    its associated indirect jumps and stack traffic only occur when
+    BufferBuilder is asked to append a non-strict ByteString.
+    .
+    I benchmarked four major implementations and benchmarked with the buildURL benchmark:
+    .
+      * State monad, concatenating ByteStrings: __6.98 us__
+    .
+      * State monad, ByteString Builder: __2.48 us__
+    .
+      * Crazy explicit RealWorld baton passing with unboxed state: __28.94 us__ (GHC generated really awful code for this, but see the revision history for the technique)
+    .
+      * C + FFI + ReaderT: __1.11 us__
+    .
+    Using BufferBuilder is very simple:
+    .
+    > import qualified Data.BufferBuilder as BB
+    > 
+    > let byteString = BB.runBufferBuilder $ do
+    >       BB.appendBS "http"
+    >       BB.appendChar8 '/'
+    >       BB.appendBS "//"
+    
+
+license:             BSD3
+license-file:        LICENSE
+author:              Chad Austin
+maintainer:          chad@chadaustin.me
+copyright:           IMVU Inc., Chad Austin
+category:            Data
+build-type:          Simple
+stability:           experimental
+homepage:            https://github.com/chadaustin/buffer-builder
+cabal-version:       >=1.10
+
+library
+  exposed-modules:
+    Data.BufferBuilder
+
+  build-depends: base ==4.*
+               , bytestring
+               , mtl
+
+  default-language: Haskell2010
+  ghc-options: -O2 -Wall
+
+  c-sources: buffer.cpp
+  cc-options: -O2 -Wall
+
+test-suite tests
+  type: exitcode-stdio-1.0
+  main-is: Main.hs
+  hs-source-dirs: test
+  default-language: Haskell2010
+  ghc-options: -O2 -Wall
+
+  build-depends: base ==4.*
+               , buffer-builder
+               , tasty
+               , tasty-hunit
+               , tasty-th
+               , HUnit
+
+benchmark bench
+  type: exitcode-stdio-1.0
+  main-is: Bench.hs
+  hs-source-dirs: bench
+  default-language: Haskell2010
+  ghc-options: -O2 -Wall -ddump-ds -ddump-simpl -ddump-stg -ddump-opt-cmm -ddump-asm -ddump-to-file
+
+  build-depends: base ==4.*
+               , bytestring
+               , buffer-builder
+               , criterion
diff --git a/buffer.cpp b/buffer.cpp
new file mode 100644
--- /dev/null
+++ b/buffer.cpp
@@ -0,0 +1,85 @@
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+const size_t TRIM_THRESHOLD = 8192; // minimum number of bytes saved to trim
+
+namespace {
+    struct BufferWriter {
+        unsigned char* data;
+        size_t size;
+        size_t capacity;
+    };
+}
+
+// assumes malloc will not fail
+// TODO: replace assert with some other error mechanism
+
+extern "C" BufferWriter* bw_new(size_t initialCapacity) {
+    assert(initialCapacity >= 1);
+    BufferWriter* bw = reinterpret_cast<BufferWriter*>(malloc(sizeof(BufferWriter)));
+    assert(bw);
+    bw->data = reinterpret_cast<unsigned char*>(malloc(initialCapacity));
+    assert(bw->data);
+    bw->size = 0;
+    bw->capacity = initialCapacity;
+    return bw;
+}
+
+extern "C" void bw_free(BufferWriter* bw) {
+    free(bw->data);
+    free(bw);
+}
+
+extern "C" void bw_append_byte(BufferWriter* bw, unsigned char byte) {
+    assert(bw->data);
+    if (bw->size >= bw->capacity) {
+        size_t newCapacity = bw->capacity * 2;
+        unsigned char* nd = reinterpret_cast<unsigned char*>(realloc(bw->data, newCapacity));
+        assert(nd);
+        bw->data = nd;
+        bw->capacity = newCapacity;
+    }
+
+    bw->data[bw->size] = byte;
+    bw->size += 1;
+}
+
+extern "C" void bw_append_bs(BufferWriter* bw, size_t size, unsigned char* data) {
+    assert(bw->data);
+    if (bw->size + size > bw->capacity) {
+        size_t newCapacity = bw->capacity * 2;
+        while (bw->size + size > newCapacity) {
+            newCapacity *= 2;
+        }
+        unsigned char* nd = reinterpret_cast<unsigned char*>(realloc(bw->data, newCapacity));
+        assert(nd);
+        bw->data = nd;
+        bw->capacity = newCapacity;
+    }
+
+    memcpy(bw->data + bw->size, data, size);
+    bw->size += size;
+}
+
+extern "C" size_t bw_get_size(BufferWriter* bw) {
+    return bw->size;
+}
+
+extern "C" unsigned char* bw_trim_and_release_address(BufferWriter* bw) {
+    unsigned char* data = bw->data;
+    
+    if (bw->size + TRIM_THRESHOLD < bw->capacity) {
+        // try to shrink
+        data = reinterpret_cast<unsigned char*>(realloc(data, bw->size));
+        if (!data) {
+            // no problem
+            data = bw->data;
+        }
+    }
+
+    bw->data = 0;
+    return data;
+}
+
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,29 @@
+{-# LANGUAGE TemplateHaskell, OverloadedStrings #-}
+
+import Test.Tasty
+import Test.Tasty.TH
+import Test.Tasty.HUnit
+import Data.BufferBuilder
+
+case_append_bytes :: Assertion
+case_append_bytes = do
+    let result = runBufferBuilder $ do
+            appendChar8 'f'
+            appendChar8 'o'
+            appendChar8 'o'
+    assertEqual "matches" "foo" result
+
+case_append_string :: Assertion
+case_append_string = do
+    let result = runBufferBuilder $ do
+            appendChar8 'f'
+            appendChar8 'o'
+            appendChar8 'o'
+            appendBS "bar"
+    assertEqual "matches" "foobar" result
+
+tests :: TestTree
+tests = $(testGroupGenerator)
+
+main :: IO ()
+main = defaultMain tests
