Repository: Xilinx/inference-server (language: python)

Issue:
Reduce boilerplate
Many of the examples/tests/src have repetitive boiler plate code that should be pulled out instead of being copied


Discussion / hints:

Fix patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c2ee771ff..cb03156ff 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -28,13 +28,11 @@ if(result_len EQUAL "4")
   string(SUBSTRING ${ver_label} 1 ${ver_label_len} ver_label)
   message(
     STATUS
-      "Building Proteus version ${ver_major}.${ver_minor}.${ver_patch}-${ver_label}"
+      "Building version ${ver_major}.${ver_minor}.${ver_patch}-${ver_label}"
   )
 else()
   set(ver_label "")
-  message(
-    STATUS "Building Proteus version ${ver_major}.${ver_minor}.${ver_patch}"
-  )
+  message(STATUS "Building version ${ver_major}.${ver_minor}.${ver_patch}")
 endif()
 
 # set the project name
@@ -123,10 +121,7 @@ if(${xir_FOUND}
 else()
   set(PROTEUS_VITIS_FOUND OFF)
   set(PROTEUS_AKS_FOUND OFF)
-  message(
-    STATUS
-      "One or more Vitis dependencies weren't found. Disabling all Vitis dependencies"
-  )
+  message(STATUS "One or more Vitis dependencies not found. Disabling Vitis")
 endif()
 
 configure_file(
diff --git a/Dockerfile b/Dockerfile
index 26479c962..70c991939 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -893,6 +893,7 @@ RUN apt-get update \
         curl \
         make \
         # used for git
+        gpg \
         openssh-client \
         python3 \
         python3-dev \
diff --git a/LICENSE b/LICENSE
index 302560c5a..05511a704 100644
--- a/LICENSE
+++ b/LICENSE
@@ -240,7 +240,7 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 --------------------------------------------------------------------------------
-For src/proteus/helpers/ctpl.h
+For src/proteus/util/ctpl.h
 
 Copyright (C) 2014 by Vitaliy Vitsentiy
 Copyright (C) 2021 Xilinx Inc.
diff --git a/docker/.bashrc b/docker/.bashrc
index 0c5090a04..fcb88fd66 100644
--- a/docker/.bashrc
+++ b/docker/.bashrc
@@ -154,6 +154,8 @@ if [ $TFZENDNN_FOUND -eq 1 ] || [ $PTZENDNN_FOUND -eq 1 ]; then
     source ${PROTEUS_ROOT}/external/zendnn/zendnn_tf_pt_cc_api_env_setup.sh
 fi
 
+export GPG_TTY=$(tty)
+
 clear
 print_banner
 
diff --git a/examples/cpp/CMakeLists.txt b/examples/cpp/CMakeLists.txt
index 6a58406df..7eb75c28f 100644
--- a/examples/cpp/CMakeLists.txt
+++ b/examples/cpp/CMakeLists.txt
@@ -21,9 +21,9 @@ if(${PROTEUS_ENABLE_VITIS})
     add_executable(${app} custom_processing.cpp)
 
     target_link_libraries(
-      ${app} PRIVATE proteus opencv_imgcodecs opencv_imgproc opencv_core
+      ${app} PRIVATE proteus util opencv_imgcodecs opencv_imgproc opencv_core
     )
-    target_include_directories(${app} PRIVATE ${PROTEUS_PUBLIC_INCLUDE_DIRS})
+    target_include_directories(${app} PRIVATE ${PROTEUS_INCLUDE_DIRS})
   endforeach()
   if(${PROTEUS_ENABLE_GRPC})
     target_compile_definitions(custom_processing_grpc PRIVATE ENABLE_GRPC)
@@ -34,35 +34,28 @@ if(${PROTEUS_ENABLE_TFZENDNN})
   add_executable(tf_zendnn_client tf_zendnn_client.cpp)
 
   target_link_libraries(
-    tf_zendnn_client PRIVATE proteus opencv_imgcodecs opencv_imgproc
+    tf_zendnn_client PRIVATE proteus util opencv_imgcodecs opencv_imgproc
                              opencv_core
   )
-  target_include_directories(
-    tf_zendnn_client PRIVATE ${PROTEUS_PUBLIC_INCLUDE_DIRS}
-  )
+  target_include_directories(tf_zendnn_client PRIVATE ${PROTEUS_INCLUDE_DIRS})
 endif()
 
 if(${PROTEUS_ENABLE_PTZENDNN})
   add_executable(pt_zendnn_client pt_zendnn_client.cpp)
 
   target_link_libraries(
-    pt_zendnn_client PRIVATE proteus opencv_imgcodecs opencv_imgproc
+    pt_zendnn_client PRIVATE proteus util opencv_imgcodecs opencv_imgproc
                              opencv_core
   )
-  target_include_directories(
-    pt_zendnn_client PRIVATE ${PROTEUS_PUBLIC_INCLUDE_DIRS}
-  )
+  target_include_directories(pt_zendnn_client PRIVATE ${PROTEUS_INCLUDE_DIRS})
 endif()
 
-
 if(${PROTEUS_ENABLE_MIGRAPHX})
   add_executable(migraphx_client migraphx_client.cpp)
 
   target_link_libraries(
-    migraphx_client PRIVATE proteus opencv_imgcodecs opencv_imgproc
-                             opencv_core migraphx::c
-  )
-  target_include_directories(
-    migraphx_client PRIVATE ${PROTEUS_PUBLIC_INCLUDE_DIRS}
+    migraphx_client PRIVATE proteus util opencv_imgcodecs opencv_imgproc
+                            opencv_core migraphx::c
   )
+  target_include_directories(migraphx_client PRIVATE ${PROTEUS_INCLUDE_DIRS})
 endif()
diff --git a/examples/cpp/custom_processing.cpp b/examples/cpp/custom_processing.cpp
index b2d8540a0..adc9656dd 100644
--- a/examples/cpp/custom_processing.cpp
+++ b/examples/cpp/custom_processing.cpp
@@ -38,135 +38,8 @@
 #include "proteus/proteus.hpp"
 // -include:
 
-/**
- * @brief Perform preprocessing for the input images
- *
- * @param paths paths to images to preprocess
- * @return std::vector<std::vector<int8_t>> preprocessed image data
- */
-std::vector<std::vector<int8_t>> preprocess(
-  const std::vector<std::string>& paths) {
-  // update as needed!
-  const auto height = 224;
-  const auto width = 224;
-  const auto channels = 3;
-  std::string layout = "NHWC";
-  const int8_t fix_scale = 1;
-  const std::array<int8_t, 3> mean = {123, 107, 104};
-
-  std::vector<std::vector<int8_t>> outputs;
-  outputs.reserve(paths.size());
-
-  auto index = 0;
-  for (const auto& path : paths) {
-    auto img = cv::imread(path);
-
-    cv::Mat resizedImg = cv::Mat(height, width, CV_8SC3);
-    cv::resize(img, resizedImg, cv::Size(width, height));
-    auto size = resizedImg.size[0] * resizedImg.size[1] * channels;
-    outputs.emplace_back();
-    auto& output = outputs[index];
-    output.resize(size);
-
-    if (layout == "NCHW") {
-      for (int c = 0; c < channels; c++) {
-        for (int h = 0; h < height; h++) {
-          for (int w = 0; w < width; w++) {
-            output[(c * height * width) + (h * width) + w] =
-              (static_cast<int8_t>(resizedImg.at<cv::Vec3b>(h, w)[c]) -
-               mean.at(c)) *
-              fix_scale;
-          }
-        }
-      }
-    } else if (layout == "NHWC") {
-      for (int h = 0; h < height; h++) {
-        for (int w = 0; w < width; w++) {
-          for (int c = 0; c < channels; c++) {
-            output[(h * width * channels) + (w * channels) + c] =
-              (static_cast<int8_t>(resizedImg.at<cv::Vec3b>(h, w)[c]) -
-               mean.at(c)) *
-              fix_scale;
-          }
-        }
-      }
-    }
-    index++;
-  }
-  return outputs;
-}
-
-/**
- * @brief Calculate softmax of the data
- *
- * @tparam T the expected type of the data
- * @param data pointer to the raw data
- * @param size number of elements in the raw data
- * @param result pointer to store the computed results
- */
-template <typename T>
-void calc_softmax(const T* data, size_t size, double* result) {
-  double sum = 0;
-
-  auto max = data[0];
-  for (size_t i = 1; i < size; i++) {
-    if (data[i] > max) {
-      max = data[i];
-    }
-  }
-
-  for (size_t i = 0; i < size; i++) {
-    result[i] = exp(data[i] - max);
-    sum += result[i];
-  }
-
-  for (size_t i = 0; i < size; i++) {
-    result[i] /= sum;
-  }
-}
-
-/**
- * @brief After running softmax, get the labels associated with the top k values
- *
- * @param d pointer to the data
- * @param size number of elements in the data
- * @param k number of top elements to return
- * @return std::vector<int>
- */
-std::vector<int> get_top_k(const double* d, int size, int k) {
-  std::priority_queue<std::pair<float, int>> q;
-
-  for (auto i = 0; i < size; ++i) {
-    q.push(std::pair<float, int>(d[i], i));
-  }
-  std::vector<int> topKIndex;
-  for (auto i = 0; i < k; ++i) {
-    std::pair<float, int> ki = q.top();
-    q.pop();
-    topKIndex.push_back(ki.second);
-  }
-  return topKIndex;
-}
-
-/**
- * @brief Perform postprocessing of the data
- *
- * @tparam T the expected type of the data
- * @param output output from Proteus
- * @param k number of top elements to return
- * @return std::vector<int>
- */
-template <typename T>
-std::vector<int> postprocess(proteus::InferenceResponseOutput& output, int k) {
-  auto* data = static_cast<std::vector<T>*>(output.getData());
-  auto size = output.getSize();
-
-  std::vector<double> softmax;
-  softmax.resize(size, 0);
-
-  calc_softmax<T>(data->data(), size, softmax.data());
-  return get_top_k(softmax.data(), size, k);
-}
+#include "proteus/util/pre_post/image_preprocess.hpp"
+#include "proteus/util/pre_post/resnet50_postprocess.hpp"
 
 #ifdef ENABLE_GRPC
 constexpr auto GRPC_PORT = 50051;
@@ -206,8 +79,10 @@ proteus::InferenceResponse infer(const std::string& worker_name,
   return results;
 }
 
-std::vector<int> postprocess(proteus::InferenceResponseOutput& output, int k) {
-  return postprocess<int32_t>(output, k);
+std::vector<int> postprocess(const proteus::InferenceResponseOutput& output,
+                             int k) {
+  return proteus::util::resnet50Postprocess(
+    static_cast<int32_t*>(output.getData()), output.getSize(), k);
 }
 #else
 std::string load(const std::string& path_to_xmodel) {
@@ -237,8 +112,10 @@ proteus::InferenceResponse infer(const std::string& worker_name,
   return response;
 }
 
-std::vector<int> postprocess(proteus::InferenceResponseOutput& output, int k) {
-  return postprocess<int8_t>(output, k);
+std::vector<int> postprocess(const proteus::InferenceResponseOutput& output,
+                             int k) {
+  return proteus::util::resnet50Postprocess(
+    static_cast<int8_t*>(output.getData()), output.getSize(), k);
 }
 #endif
 
@@ -285,7 +162,12 @@ int main() {
   paths.reserve(request_num);
   paths.emplace_back(path_to_image);
 
-  auto images = preprocess(paths);
+  proteus::util::ImagePreprocessOptions<int8_t, 3> options;
+  options.order = proteus::util::ImageOrder::NHWC;
+  options.mean = {123, 107, 104};
+  options.std = {1, 1, 1};
+  options.normalize = true;
+  auto images = proteus::util::imagePreprocess(paths, options);
   // -prepare images:
 
   // +construct request:
diff --git a/examples/cpp/migraphx_client.cpp b/examples/cpp/migraphx_client.cpp
index 2ebc66622..f329119ed 100644
--- a/examples/cpp/migraphx_client.cpp
+++ b/examples/cpp/migraphx_client.cpp
@@ -32,6 +32,9 @@
 #include <vector>                 // for vector
 
 #include "proteus/proteus.hpp"  // for InferenceResponseFuture, terminate
+#include "proteus/util/pre_post/image_preprocess.hpp"
+#include "proteus/util/pre_post/resnet50_postprocess.hpp"
+#include "proteus/util/read_nth_line.hpp"
 
 // // #include "migraphx/filesystem.hpp"
 // #pragma GCC diagnostic push
@@ -39,170 +42,6 @@
 // #pragma GCC diagnostic ignored "-Wpedantic"
 // #include "migraphx/migraphx.hpp"  // MIGraphX C++ API
 // #pragma GCC diagnostic pop
-/**
- * @brief This method will read the class file and returns class name
- *
- * @param filename: The location of the file containing class names
- * @param N: Nth value from the class file
- * @return std::string: Name of the class
- */
-std::string ReadNthLine(const std::string filename, int N) {
-  std::ifstream in(filename);
-  std::string line;
-  line.reserve(100);  // for performance
-  // skip N lines
-  for (int i = 0; i < N; ++i) std::getline(in, line);
-
-  std::getline(in, line);
-  return line;
-}
-
-/**
- * @brief Image preprocessing helper.  Crops the input `img` from center to a
- * specific dimension specified
- *
- * @param img: Image which needs to be cropped
- * @param height: height of the output image
- * @param width: width of the output image
- * @return cv::Mat: Image cropped to required shape
- */
-cv::Mat center_crop(cv::Mat img, int height, int width) {
-  const int offsetW = (img.cols - width) / 2;
-  const int offsetH = (img.rows - height) / 2;
-  const cv::Rect roi(offsetW, offsetH, width, height);
-  img = img(roi);
-  return img;
-}
-
-/**
- * @brief Utility function:  fetches an image or images from file and returns an
- * array that can be passed as data to the migraphx worker.
- *
- * @param paths
- * @param img_size
- * @param method
- * @return std::vector<std::vector<float>>
- */
-std::vector<std::vector<float>> preprocess(
-  std::filesystem::path const& image_filename, size_t batch_size,
-  size_t image_size) {
-  // placeholder
-  std::vector<std::vector<float>> outputs;
-  outputs.reserve(batch_size);
-
-  // load the image
-  auto img = cv::imread(image_filename.c_str());
-  if (img.empty()) {
-    std::cout << "Unable to load image " << image_filename.c_str() << std::endl;
-    exit(1);
-  }
-  //
-  // Preprocess
-  //
-  std::vector<float> output1;
-
-  cv::cvtColor(img, img, cv::COLOR_BGR2RGB);
-  auto new_size = cv::Size(256, 256);
-  cv::resize(img, img, new_size, cv::INTER_LINEAR);
-  img = center_crop(img, image_size, image_size);
-  img.convertTo(img, CV_32FC3, 1.0 / 255.0);
-  img = img.isContinuous() ? img : img.clone();
-
-  const std::array mean = {0.485, 0.456, 0.406};
-  const std::array std = {0.229, 0.224, 0.225};
-
-  // Copy to the vector, normalizing each pixel
-  auto size = img.size[0] * img.size[1] * 3;
-  output1.resize(size);
-  for (size_t c = 0; c < 3; c++) {
-    for (size_t h = 0; h < image_size; h++) {
-      for (size_t w = 0; w < image_size; w++) {
-        output1[(c * image_size * image_size) + (h * image_size) + w] =
-          (img.at<cv::Vec3f>(h, w)[c] - mean.at(c)) / std.at(c);
-      }
-    }
-  }
-  for (size_t index = 0; index < batch_size; index++) {
-    outputs.emplace_back(output1);
-    // auto& output = outputs[index++];
-  }
-  std::cout << "preprocess() returning vector of "
-            << std::to_string(outputs.size()) << " images of "
-            << outputs[batch_size - 1].size() << std::endl;
-
-  return outputs;
-}
-
-/**
- * @brief Calculate softmax of the data.
- *         This is identical to processing in pt_zendnn_client.cpp
- *
- * @param data pointer to the raw data
- * @param size number of elements in the raw data
- * @param result pointer to store the computed results
- */
-void calc_softmax(const float* data, size_t size, double* result) {
-  double sum = 0;
-
-  auto max = data[0];
-  for (size_t i = 1; i < size; i++) {
-    if (data[i] > max) {
-      max = data[i];
-    }
-  }
-
-  for (size_t i = 0; i < size; i++) {
-    result[i] = exp(data[i] - max);
-    sum += result[i];
-  }
-
-  for (size_t i = 0; i < size; i++) {
-    result[i] /= sum;
-  }
-}
-
-/**
- * @brief After running softmax, get the labels associated with the top k
- * values. This is identical to processing in pt_zendnn_client.cpp
- *
- * @param d pointer to the data
- * @param size number of elements in the data
- * @param k number of top elements to return
- * @return std::vector<int>
- */
-std::vector<int> get_top_k(const double* d, int size, int k) {
-  std::priority_queue<std::pair<float, int>> q;
-
-  for (auto i = 0; i < size; ++i) {
-    q.push(std::pair<float, int>(d[i], i));
-  }
-  std::vector<int> topKIndex;
-  for (auto i = 0; i < k; ++i) {
-    std::pair<float, int> ki = q.top();
-    q.pop();
-    topKIndex.push_back(ki.second);
-  }
-  return topKIndex;
-}
-
-/**
- * @brief Perform postprocessing of the data. This is identical to processing in
- * pt_zendnn_client.cpp
- *
- * @param output output from Proteus
- * @param k number of top elements to return
- * @return std::vector<int>
- */
-std::vector<int> postprocess(proteus::InferenceResponseOutput& output, int k) {
-  auto* data = static_cast<std::vector<float>*>(output.getData());
-  auto size = output.getSize();
-
-  std::vector<double> softmax;
-  softmax.resize(size);
-
-  calc_softmax(data->data(), size, softmax.data());
-  return get_top_k(softmax.data(), size, k);
-}
 
 /**
  * @brief user variables: update as needed!
@@ -214,7 +53,7 @@ struct Option {
   std::filesystem::path root = std::getenv("PROTEUS_ROOT");
 
   std::filesystem::path model =
-    root / "external/artifacts/migraphx/resnet50-v1-7/resnet50-v1-7.onnx";
+    root / "external/artifacts/migraphx/resnet50v2/resnet50-v2-7.onnx";
   std::filesystem::path image_location =
     root / "tests/assets/dog-3619020_640.jpg";
 
@@ -228,7 +67,7 @@ struct Option {
 
   // values used in iterations and postprocessing
   int output_classes = 1000;
-  int steps = 10;
+  int steps = 1;
   int topK = 5;
 } options;
 
@@ -250,7 +89,7 @@ int main(int argc, char* argv[]) {
   std::filesystem::path root(root_env);
 
   std::filesystem::path model =
-    root / "external/artifacts/migraphx/resnet50-v1-7/resnet50-v1-7.onnx";
+    root / "external/artifacts/migraphx/resnet50v2/resnet50-v2-7.onnx";
   if (argc > 1) model = std::string(argv[1]);
 
   int batch_size(64);
@@ -260,6 +99,8 @@ int main(int argc, char* argv[]) {
   // The image file path is hardcoded.  For performance testing we'll just
   // process multiple copies of the same image.
   std::filesystem::path image_filename = options.image_location;
+  std::vector<std::string> paths;
+  paths.push_back(image_filename.string());
   std::cout << " input image is " << image_filename << "   batch size is "
             << std::to_string(batch_size) << std::endl;
 
@@ -287,7 +128,17 @@ int main(int argc, char* argv[]) {
   std::cout << "loaded worker name " << workerName << std::endl;
 
   // Load the image and make an array of copies
-  auto images = preprocess(image_filename, batch_size, 224);
+  proteus::util::ImagePreprocessOptions<float, 3> options_;
+  options_.normalize = true;
+  options_.order = proteus::util::ImageOrder::NCHW;
+  options_.mean = {0.485, 0.456, 0.406};
+  options_.std = {4.367, 4.464, 4.444};
+  options_.convert_color = true;
+  options_.color_code = cv::COLOR_BGR2RGB;
+  options_.convert_type = true;
+  options_.type = CV_32FC3;
+  options_.convert_scale = 1.0 / 255.0;
+  auto images = proteus::util::imagePreprocess(paths, options_);
 
   const std::initializer_list<uint64_t> shape = {
     3, static_cast<long unsigned>(options.input_size),
@@ -307,11 +158,12 @@ int main(int argc, char* argv[]) {
   // Parse the results and report the top K results
   auto outputs = results.getOutputs();
   for (auto& output : outputs) {
-    auto top_k = postprocess(output, options.topK);
+    auto top_k = proteus::util::resnet50Postprocess(
+      static_cast<float*>(output.getData()), output.getSize(), options.topK);
     std::cout << "Top " << options.topK << " classes:" << std::endl;
     for (int j = 0; j < options.topK; j++)
-      std::cout << j + 1 << ": Index: " << top_k[j]
-                << " :: Class: " << ReadNthLine(options.class_file, top_k[j])
+      std::cout << j + 1 << ": Index: " << top_k[j] << " :: Class: "
+                << proteus::util::readNthLine(options.class_file, top_k[j])
                 << std::endl;
   }
 
@@ -320,7 +172,7 @@ int main(int argc, char* argv[]) {
   for (int step = 0; step < options.steps; step++) {
     for (auto i = 0; i < options.batch_size; i++) {
       proteus::InferenceRequest request;
-      request.addInputTensor(static_cast<void*>(images[i].data()), shape,
+      request.addInputTensor(static_cast<void*>(images[0].data()), shape,
                              proteus::DataType::FP32);
       auto results = client.modelInfer(workerName, request);
     }
@@ -389,4 +241,4 @@ int main(int argc, char* argv[]) {
 // We should see 1000 items returned.
 // std::cout << "done.  elem is " << std::to_string(elem_num) << std::endl;
 // }
-// clang-format on
\ No newline at end of file
+// clang-format on
diff --git a/examples/cpp/pt_zendnn_client.cpp b/examples/cpp/pt_zendnn_client.cpp
index 740b4eb26..1ebb46663 100644
--- a/examples/cpp/pt_zendnn_client.cpp
+++ b/examples/cpp/pt_zendnn_client.cpp
@@ -32,162 +32,9 @@
 #include <vector>                 // for vector
 
 #include "proteus/proteus.hpp"  // for InferenceResponseFuture, terminate
-
-/**
- * @brief This method will read the class file and returns class name
- *
- * @param filename: The location of the file containing class names
- * @param N: Nth value from the class file
- * @return std::string: Name of the class
- */
-std::string ReadNthLine(const std::string filename, int N) {
-  std::ifstream in(filename);
-  std::string line;
-  line.reserve(100);  // for performance
-  // skip N lines
-  for (int i = 0; i < N; ++i) std::getline(in, line);
-
-  std::getline(in, line);
-  return line;
-}
-
-/**
- * @brief Crops the input `img` from center to a specific dimension specified
- *
- * @param img: Image which needs to be cropped
- * @param height: height of the output image
- * @param width: width of the output image
- * @return cv::Mat: Image cropped to required shape
- */
-cv::Mat center_crop(cv::Mat img, int height, int width) {
-  const int offsetW = (img.cols - width) / 2;
-  const int offsetH = (img.rows - height) / 2;
-  const cv::Rect roi(offsetW, offsetH, width, height);
-  img = img(roi);
-  return img;
-}
-
-/**
- * @brief This image will load and preprocess the data according to
- * the input arguments
- *
- * @param paths: The location of the image to be loaded
- * @param img_size: The input height/width of the image
- * @return std::vector<std::vector<float>>: The preprocessed image
- */
-std::vector<std::vector<float>> preprocess(
-  const std::vector<std::string>& paths, int img_size,
-  std::string method = "default") {
-  // update as needed!
-
-  std::vector<std::vector<float>> outputs;
-  outputs.reserve(paths.size());
-
-  auto index = 0;
-  for (const auto& path : paths) {
-    auto img = cv::imread(path);
-    cv::cvtColor(img, img, cv::COLOR_BGR2RGB);
-
-    outputs.emplace_back();
-    auto& output = outputs[index++];
-
-    if (!method.compare("default")) {
-      // Preprocess
-      auto new_size = cv::Size(256, 256);
-      cv::resize(img, img, new_size, cv::INTER_LINEAR);
-      img = center_crop(img, img_size, img_size);
-      img.convertTo(img, CV_32FC3, 1.0 / 255.0);
-      img = img.isContinuous() ? img : img.clone();
-
-      const std::array mean = {0.485, 0.456, 0.406};
-      const std::array std = {0.229, 0.224, 0.225};
-
-      // Copy to the vector
-      auto size = img.size[0] * img.size[1] * 3;
-      output.resize(size);
-      for (int c = 0; c < 3; c++) {
-        for (int h = 0; h < img_size; h++) {
-          for (int w = 0; w < img_size; w++) {
-            output[(c * img_size * img_size) + (h * img_size) + w] =
-              (img.at<cv::Vec3f>(h, w)[c] - mean.at(c)) / std.at(c);
-          }
-        }
-      }
-    } else {
-      std::cerr << "Preprocess algo " << method << " not implemented";
-      exit(0);
-    }
-  }
-  return outputs;
-}
-
-/**
- * @brief Calculate softmax of the data
- *
- * @param data pointer to the raw data
- * @param size number of elements in the raw data
- * @param result pointer to store the computed results
- */
-void calc_softmax(const float* data, size_t size, double* result) {
-  double sum = 0;
-
-  auto max = data[0];
-  for (size_t i = 1; i < size; i++) {
-    if (data[i] > max) {
-      max = data[i];
-    }
-  }
-
-  for (size_t i = 0; i < size; i++) {
-    result[i] = exp(data[i] - max);
-    sum += result[i];
-  }
-
-  for (size_t i = 0; i < size; i++) {
-    result[i] /= sum;
-  }
-}
-
-/**
- * @brief After running softmax, get the labels associated with the top k values
- *
- * @param d pointer to the data
- * @param size number of elements in the data
- * @param k number of top elements to return
- * @return std::vector<int>
- */
-std::vector<int> get_top_k(const double* d, int size, int k) {
-  std::priority_queue<std::pair<float, int>> q;
-
-  for (auto i = 0; i < size; ++i) {
-    q.push(std::pair<float, int>(d[i], i));
-  }
-  std::vector<int> topKIndex;
-  for (auto i = 0; i < k; ++i) {
-    std::pair<float, int> ki = q.top();
-    q.pop();
-    topKIndex.push_back(ki.second);
-  }
-  return topKIndex;
-}
-
-/**
- * @brief Perform postprocessing of the data
- *
- * @param output output from Proteus
- * @param k number of top elements to return
- * @return std::vector<int>
- */
-std::vector<int> postprocess(proteus::InferenceResponseOutput& output, int k) {
-  auto* data = static_cast<std::vector<float>*>(output.getData());
-  auto size = output.getSize();
-
-  std::vector<double> softmax;
-  softmax.resize(size);
-
-  calc_softmax(data->data(), size, softmax.data());
-  return get_top_k(softmax.data(), size, k);
-}
+#include "proteus/util/pre_post/image_preprocess.hpp"
+#include "proteus/util/pre_post/resnet50_postprocess.hpp"
+#include "proteus/util/read_nth_line.hpp"
 
 /**
  * @brief The custom processing example demonstrates how to plug in custom pre-
@@ -217,7 +64,7 @@ struct Option {
   int input_size = 224;
   int output_classes = 1000;
   int warmup_step = 5;
-  int steps = 10;
+  int steps = 1;
   int topK = 5;
 } options;
 
@@ -260,7 +107,17 @@ int main() {
   if (!options.image_location.empty()) {
     std::vector<std::string> paths;
     paths.emplace_back(options.image_location);
-    auto images = preprocess(paths, options.input_size, options.resize_method);
+    proteus::util::ImagePreprocessOptions<float, 3> options_;
+    options_.normalize = true;
+    options_.order = proteus::util::ImageOrder::NCHW;
+    options_.mean = {0.485, 0.456, 0.406};
+    options_.std = {4.367, 4.464, 4.444};
+    options_.convert_color = true;
+    options_.color_code = cv::COLOR_BGR2RGB;
+    options_.convert_type = true;
+    options_.type = CV_32FC3;
+    options_.convert_scale = 1.0 / 255.0;
+    auto images = proteus::util::imagePreprocess(paths, options_);
 
     const std::initializer_list<uint64_t> shape = {
       3, static_cast<uint64_t>(options.input_size),
@@ -279,11 +136,12 @@ int main() {
 
     auto outputs = results.getOutputs();
     for (auto& output : outputs) {
-      auto top_k = postprocess(output, options.topK);
+      auto top_k = proteus::util::resnet50Postprocess(
+        static_cast<float*>(output.getData()), output.getSize(), options.topK);
       std::cout << "Top " << options.topK << " classes:" << std::endl;
       for (int j = 0; j < options.topK; j++)
-        std::cout << j + 1 << ": Index: " << top_k[j]
-                  << " :: Class: " << ReadNthLine(options.class_file, top_k[j])
+        std::cout << j + 1 << ": Index: " << top_k[j] << " :: Class: "
+                  << proteus::util::readNthLine(options.class_file, top_k[j])
                   << std::endl;
     }
   } else {
diff --git a/examples/cpp/tf_zendnn_client.cpp b/examples/cpp/tf_zendnn_client.cpp
index ae65950c5..c5474203b 100644
--- a/examples/cpp/tf_zendnn_client.cpp
+++ b/examples/cpp/tf_zendnn_client.cpp
@@ -36,174 +36,9 @@
 #include <vector>                 // for vector
 
 #include "proteus/proteus.hpp"  // for InferenceResponseFuture, terminate
-
-/**
- * @brief This method will read the class file and returns class name
- *
- * @param filename: The location of the file containing class names
- * @param N: Nth value from the class file
- * @return std::string: Name of the class
- */
-std::string ReadNthLine(const std::string filename, int N) {
-  std::ifstream in(filename);
-  std::string line;
-  line.reserve(100);  // for performance
-  // skip N lines
-  for (int i = 0; i < N; ++i) std::getline(in, line);
-
-  std::getline(in, line);
-  return line;
-}
-
-/**
- * @brief Crops the input `img` from center to a specific dimension specified
- *
- * @param img: Image which needs to be cropped
- * @param height: height of the output image
- * @param width: width of the output image
- * @return cv::Mat: Image cropped to required shape
- */
-cv::Mat center_crop(cv::Mat img, int height, int width) {
-  const int offsetW = (img.cols - width) / 2;
-  const int offsetH = (img.rows - height) / 2;
-  const cv::Rect roi(offsetW, offsetH, width, height);
-  img = img(roi);
-  return img;
-}
-
-/**
- * @brief This image will load and preprocess the data according to
- * the input arguments
- *
- * @param paths: The location of the image to be loaded
- * @param img_size: The input height/width of the image
- * @param method: Which prepcessing method to use
- * @return std::vector<std::vector<float>>: The preprocessed image
- */
-std::vector<std::vector<float>> preprocess(
-  const std::vector<std::string>& paths, int img_size,
-  std::string method = "crop") {
-  // update as needed!
-
-  std::vector<std::vector<float>> outputs;
-  outputs.reserve(paths.size());
-
-  auto index = 0;
-  for (const auto& path : paths) {
-    auto img = cv::imread(path);
-    cv::cvtColor(img, img, cv::COLOR_BGR2RGB);
-
-    outputs.emplace_back();
-    auto& output = outputs[index++];
-
-    if (!method.compare("crop") || !method.compare("xilinx")) {
-      // Preprocess
-      int img_height = img.rows;
-      int img_width = img.cols;
-      float scale = 1.f;
-      if (img_height < img_width)
-        scale = 256.f / img_height;
-      else
-        scale = 256.f / img_width;
-      auto new_size = cv::Size(img_width * scale, img_height * scale);
-      cv::resize(img, img, new_size, cv::INTER_LINEAR);
-      img = center_crop(img, img_size, img_size);
-
-      // Copy to the vector
-      auto size = img.size[0] * img.size[1] * 3;
-      output.resize(size);
-      uchar* arr = img.isContinuous() ? img.data : img.clone().data;
-      output.assign(arr, arr + size);
-    } else if (!method.compare("bilinear")) {
-      // Preprocess
-      img.convertTo(img, CV_32FC3, 1.0 / 255.0);
-      img = center_crop(img, img.rows * 0.875, img.cols * 0.875);
-      auto new_size = cv::Size(img_size, img_size);
-      cv::resize(img, img, new_size, cv::INTER_LINEAR);
-      img = img - 0.5;
-      img = img * 2;
-
-      // Copy to the vector
-      auto size = img.size[0] * img.size[1] * 3;
-      output.resize(size);
-      float* arr =
-        img.isContinuous() ? img.ptr<float>(0) : img.clone().ptr<float>(0);
-      output.assign(arr, arr + size);
-    } else {
-      std::cerr << "Preprocess algo " << method << " not implemented";
-      exit(0);
-    }
-  }
-  return outputs;
-}
-
-/**
- * @brief Calculate softmax of the data
- *
- * @param data pointer to the raw data
- * @param size number of elements in the raw data
- * @param result pointer to store the computed results
- */
-void calc_softmax(const float* data, size_t size, double* result) {
-  double sum = 0;
-
-  auto max = data[0];
-  for (size_t i = 1; i < size; i++) {
-    if (data[i] > max) {
-      max = data[i];
-    }
-  }
-
-  for (size_t i = 0; i < size; i++) {
-    result[i] = exp(data[i] - max);
-    sum += result[i];
-  }
-
-  for (size_t i = 0; i < size; i++) {
-    result[i] /= sum;
-  }
-}
-
-/**
- * @brief After running softmax, get the labels associated with the top k values
- *
- * @param d pointer to the data
- * @param size number of elements in the data
- * @param k number of top elements to return
- * @return std::vector<int>
- */
-std::vector<int> get_top_k(const double* d, int size, int k) {
-  std::priority_queue<std::pair<float, int>> q;
-
-  for (auto i = 0; i < size; ++i) {
-    q.push(std::pair<float, int>(d[i], i));
-  }
-  std::vector<int> topKIndex;
-  for (auto i = 0; i < k; ++i) {
-    std::pair<float, int> ki = q.top();
-    q.pop();
-    topKIndex.push_back(ki.second);
-  }
-  return topKIndex;
-}
-
-/**
- * @brief Perform postprocessing of the data
- *
- * @param output output from Proteus
- * @param k number of top elements to return
- * @return std::vector<int>
- */
-std::vector<int> postprocess(proteus::InferenceResponseOutput& output, int k) {
-  auto* data = static_cast<std::vector<float>*>(output.getData());
-  auto size = output.getSize();
-
-  std::vector<double> softmax;
-  softmax.resize(size);
-
-  calc_softmax(data->data(), size, softmax.data());
-  return get_top_k(softmax.data(), size, k);
-}
+#include "proteus/util/pre_post/image_preprocess.hpp"
+#include "proteus/util/pre_post/resnet50_postprocess.hpp"
+#include "proteus/util/read_nth_line.hpp"
 
 /**
  * @brief user variables: update as needed!
@@ -228,7 +63,7 @@ struct Option {
   int inter_op = 64;
   int intra_op = 1;
   int warmup_step = 5;
-  int steps = 10;
+  int steps = 1;
   int topK = 5;
 } options;
 
@@ -274,7 +109,11 @@ int main() {
   if (!options.image_location.empty()) {
     std::vector<std::string> paths;
     paths.emplace_back(options.image_location);
-    auto images = preprocess(paths, options.input_size, options.resize_method);
+    proteus::util::ImagePreprocessOptions<float, 3> options_;
+    options_.convert_color = true;
+    options_.color_code = cv::COLOR_BGR2RGB;
+    options_.assign = true;
+    auto images = proteus::util::imagePreprocess(paths, options_);
 
     const std::initializer_list<uint64_t> shape = {
       static_cast<uint64_t>(options.input_size),
@@ -293,11 +132,12 @@ int main() {
 
     auto outputs = results.getOutputs();
     for (auto& output : outputs) {
-      auto top_k = postprocess(output, options.topK);
+      auto top_k = proteus::util::resnet50Postprocess(
+        static_cast<float*>(output.getData()), output.getSize(), options.topK);
       std::cout << "Top " << options.topK << " classes:" << std::endl;
       for (int j = 0; j < options.topK; j++)
-        std::cout << j + 1 << ": Index: " << top_k[j]
-                  << " :: Class: " << ReadNthLine(options.class_file, top_k[j])
+        std::cout << j + 1 << ": Index: " << top_k[j] << " :: Class: "
+                  << proteus::util::readNthLine(options.class_file, top_k[j])
                   << std::endl;
     }
   } else {
diff --git a/examples/python/pt_zendnn.py b/examples/python/pt_zendnn.py
index 03c47fdf3..ba040c4dd 100644
--- a/examples/python/pt_zendnn.py
+++ b/examples/python/pt_zendnn.py
@@ -25,19 +25,6 @@
 import proteus
 
 
-def infer(client, worker_name, image):
-    """
-    Make an inference. For multiprocessing, the function must be defined at the
-    top level
-
-    Args:
-        image (np.array): Image to send to the server
-    """
-    request = proteus.ImageInferenceRequest(image)
-    response = client.modelInfer(worker_name, request)
-    assert not response.isError(), response.getError()
-
-
 def main(args):
     """
     This is the main method which
@@ -55,22 +42,9 @@ def main(args):
         FileNotFoundError: If image_location is given and the file is not found
     """
 
-    # Create server objects
-    client = proteus.clients.HttpClient("http://127.0.0.1:8998")
-
-    # Start server: if it's not already started, start it here
-    start_server = not client.serverLive()
-    if start_server:
-        server = proteus.servers.Server()
-        server.startHttp(8998)
-        while not client.serverLive():
-            time.sleep(1)
-
-    metadata = client.serverMetadata()
-
-    if not "ptzendnn" in metadata.extensions:
-        print("PTZenDNN support required but not found.")
-        sys.exit(0)
+    client, server = proteus.start_http_client_server(
+        "http://127.0.0.1:8998", "ptzendnn"
+    )
 
     # Argument parsing
     real_data = True if args.image_location else False
@@ -80,16 +54,13 @@ def main(args):
     # Validating if Image and Model are available
     if not os.path.exists(args.graph):
         raise FileNotFoundError(
-            "Model not found in the location: {}."
-            "Please Check the parameter".format(args.graph)
+            f"Model not found in the location: {args.graph}. Please check the parameter"
         )
 
-    if real_data:
-        if not os.path.exists(args.image_location):
-            raise FileNotFoundError(
-                "Image not found in the location: {}."
-                "Please Check the parameter".format(args.image_location)
-            )
+    if real_data and not os.path.exists(args.image_location):
+        raise FileNotFoundError(
+            f"Image not found in the location: {args.image_location}. Please check the parameter"
+        )
 
     # If predicting on real image, load class list from file.
     if real_data:
@@ -113,13 +84,12 @@ def main(args):
 
     # Inference with images
     # If with real data, do preprocessing, otherwise create dummy data
+    processes = int(mp.cpu_count() / 2)  # arbitrarily use half available cores
     if real_data:
         # For tests with single images
         images = []
         images.append(preprocess_pt(args.image_location, input_size))
-        request = proteus.ImageInferenceRequest(images)
-        response = client.modelInfer(worker_name, request)
-        assert not response.isError(), response.getError()
+        proteus.parallel_infer(client, worker_name, images, processes)
 
         # Post process to get top1 and top5 classes
         idx_1 = postprocess(response, 1)
@@ -142,20 +112,17 @@ def main(args):
         )
 
         # Run the inference for the images
-        processes = int(mp.cpu_count() / 2)
         while num_remaining_images >= batch_size:
 
             # create some random data
             images = np.random.uniform(
                 0.0, 255.0, (batch_size, input_size, input_size, 3)
             ).astype(np.float32)
-            images = [image for image in images]
+            images = [proteus.ImageInferenceRequest(image) for image in images]
 
             # Send request to the server
-            make_inference = functools.partial(infer, client, worker_name)
             start = time.time()
-            with mp.Pool(processes) as p:
-                p.map(make_inference, images)
+            proteus.client_operators.inferAsyncOrdered(client, worker_name, images)
             end = time.time()
             total_time += end - start
 
@@ -234,7 +201,7 @@ def main(args):
         "--steps",
         type=int,
         required=False,
-        default=5,
+        default=1,
         help="Maximum number of images (steps * batch size) used for "
         "benchmarking (only used if image_location not provided)",
     )
diff --git a/examples/python/tf_zendnn.py b/examples/python/tf_zendnn.py
index f86ef2f29..b83ec1846 100644
--- a/examples/python/tf_zendnn.py
+++ b/examples/python/tf_zendnn.py
@@ -26,19 +26,6 @@
 import proteus.clients
 
 
-def infer(client, worker_name, image):
-    """
-    Make an inference. For multiprocessing, the function must be defined at the
-    top level
-
-    Args:
-        image (np.array): Image to send to the server
-    """
-    request = proteus.ImageInferenceRequest(image)
-    response = client.modelInfer(worker_name, request)
-    assert not response.isError(), response.getError()
-
-
 def main(args):
     """
     This is the main method which
@@ -56,22 +43,9 @@ def main(args):
         FileNotFoundError: If image_location is given and the file is not found
     """
 
-    # Create server objects
-    client = proteus.clients.HttpClient("http://127.0.0.1:8998")
-
-    # Start server: if it's not already started, start it here
-    start_server = not client.serverLive()
-    if start_server:
-        server = proteus.servers.Server()
-        server.startHttp(8998)
-        while not client.serverLive():
-            time.sleep(1)
-
-    metadata = client.serverMetadata()
-
-    if not "tfzendnn" in metadata.extensions:
-        print("TFZenDNN support required but not found.")
-        sys.exit(0)
+    client, server = proteus.start_http_client_server(
+        "http://127.0.0.1:8998", "tfzendnn"
+    )
 
     # Argument parsing
     real_data = True if args.image_location else False
@@ -82,16 +56,13 @@ def main(args):
     # Validating if Image and Model are available
     if not os.path.exists(args.graph):
         raise FileNotFoundError(
-            "Model not found in the location: {}."
-            "Please Check the parameter".format(args.graph)
+            f"Model not found in the location: {args.graph}. Please check the parameter"
         )
 
-    if real_data:
-        if not os.path.exists(args.image_location):
-            raise FileNotFoundError(
-                "Image not found in the location: {}."
-                "Please Check the parameter".format(args.image_location)
-            )
+    if real_data and not os.path.exists(args.image_location):
+        raise FileNotFoundError(
+            f"Image not found in the location: {args.image_location}. Please check the parameter"
+        )
 
     # If predicting on real image, load class list from file.
     if real_data:
@@ -155,13 +126,11 @@ def main(args):
             images = np.random.uniform(
                 0.0, 255.0, (batch_size, input_size, input_size, 3)
             ).astype(np.float32)
-            images = [image for image in images]
+            images = [proteus.ImageInferenceRequest(image) for image in images]
 
             # Send request to the server
-            make_inference = functools.partial(infer, client, worker_name)
             start = time.time()
-            with mp.Pool(processes) as p:
-                p.map(make_inference, images)
+            proteus.client_operators.inferAsyncOrdered(client, worker_name, images)
             end = time.time()
             total_time += end - start
 
@@ -289,7 +258,7 @@ def main(args):
         "--steps",
         type=int,
         required=False,
-        default=5,
+        default=1,
         help="Maximum number of images (steps * batch size) used for "
         "benchmarking (only used if image_location not provided)",
     )
diff --git a/include/proteus/client_operators/infer_async.hpp b/include/proteus/client_operators/infer_async.hpp
new file mode 100644
index 000000000..4f35f1ff7
--- /dev/null
+++ b/include/proteus/client_operators/infer_async.hpp
@@ -0,0 +1,21 @@
+#ifndef GUARD_PROTEUS_CLIENT_API_INFER_ASYNC
+#define GUARD_PROTEUS_CLIENT_API_INFER_ASYNC
+
+#include <string>
+#include <vector>
+
+#include "proteus/clients/client.hpp"
+#include "proteus/core/predict_api.hpp"
+
+namespace proteus {
+
+std::vector<InferenceResponse> inferAsyncOrdered(
+  Client* client, const std::string& model,
+  const std::vector<InferenceRequest>& requests);
+std::vector<InferenceResponse> inferAsyncOrderedBatched(
+  Client* client, const std::string& model,
+  const std::vector<InferenceRequest>& requests, size_t batch_size);
+
+}  // namespace proteus
+
+#endif  // GUARD_PROTEUS_CLIENT_API_INFER_ASYNC
diff --git a/include/proteus/clients/http.hpp b/include/proteus/clients/http.hpp
index 458d90225..5bb9fa25e 100644
--- a/include/proteus/clients/http.hpp
+++ b/include/proteus/clients/http.hpp
@@ -24,9 +24,9 @@
 #include <string>  // for string
 #include <vector>  // for vector
 
-#include "proteus/clients/client.hpp"        // IWYU pragma: export
-#include "proteus/core/predict_api.hpp"      // for RequestParameters (ptr o...
-#include "proteus/helpers/declarations.hpp"  // for StringMap
+#include "proteus/clients/client.hpp"    // IWYU pragma: export
+#include "proteus/core/predict_api.hpp"  // for RequestParameters (ptr o...
+#include "proteus/declarations.hpp"      // for StringMap
 
 namespace proteus {
 
diff --git a/include/proteus/clients/native.hpp b/include/proteus/clients/native.hpp
index 0d6f7bf31..7fcd934b2 100644
--- a/include/proteus/clients/native.hpp
+++ b/include/proteus/clients/native.hpp
@@ -23,9 +23,9 @@
 #include <string>  // for string
 #include <vector>  // for vector
 
-#include "proteus/clients/client.hpp"        // IWYU pragma: export
-#include "proteus/core/predict_api.hpp"      // for InferenceRequest (ptr only)
-#include "proteus/helpers/declarations.hpp"  // for InferenceResponseFuture
+#include "proteus/clients/client.hpp"    // IWYU pragma: export
+#include "proteus/core/predict_api.hpp"  // for InferenceRequest (ptr only)
+#include "proteus/declarations.hpp"      // for InferenceResponseFuture
 
 // IWYU pragma: no_forward_declare proteus::RequestParameters
 
diff --git a/include/proteus/core/data_types.hpp b/include/proteus/core/data_types.hpp
index a88d8f317..a6df7981d 100644
--- a/include/proteus/core/data_types.hpp
+++ b/include/proteus/core/data_types.hpp
@@ -20,6 +20,7 @@
 #ifndef GUARD_PROTEUS_CORE_DATA_TYPES
 #define GUARD_PROTEUS_CORE_DATA_TYPES
 
+#include <cassert>   // for assert
 #include <cstddef>   // for size_t
 #include <cstdint>   // for uint8_t, int16_t, int32_t
 #include <iostream>  // for ostream
@@ -195,6 +196,69 @@ class DataType {
   Value value_;
 };
 
+template <typename F, typename... Args>
+auto switchOverTypes(F f, DataType type, [[maybe_unused]] const Args&... args) {
+  switch (type) {
+    case DataType::BOOL: {
+      return f.template operator()<bool>(args...);
+      break;
+    }
+    case DataType::UINT8: {
+      return f.template operator()<uint8_t>(args...);
+      break;
+    }
+    case DataType::UINT16: {
+      return f.template operator()<uint16_t>(args...);
+      break;
+    }
+    case DataType::UINT32: {
+      return f.template operator()<uint32_t>(args...);
+      break;
+    }
+    case DataType::UINT64: {
+      return f.template operator()<uint64_t>(args...);
+      break;
+    }
+    case DataType::INT8: {
+      return f.template operator()<int8_t>(args...);
+      break;
+    }
+    case DataType::INT16: {
+      return f.template operator()<int16_t>(args...);
+      break;
+    }
+    case DataType::INT32: {
+      return f.template operator()<int32_t>(args...);
+      break;
+    }
+    case DataType::INT64: {
+      return f.template operator()<int64_t>(args...);
+      break;
+    }
+    case DataType::FP16: {
+      std::cout << "FP16 not supported\n";
+      break;
+    }
+    case DataType::FP32: {
+      return f.template operator()<float>(args...);
+      break;
+    }
+    case DataType::FP64: {
+      return f.template operator()<double>(args...);
+      break;
+    }
+    case DataType::STRING: {
+      return f.template operator()<char>(args...);
+      break;
+    }
+    default:
+      // TODO(varunsh): what should we do here?
+      std::cout << "Unknown datatype\n";
+      break;
+  }
+  throw invalid_argument("Unknown datatype passed");
+}
+
 #ifdef PROTEUS_ENABLE_VITIS
 /**
  * @brief Given an XIR type, return the corresponding Proteus type
diff --git a/include/proteus/core/mixins.hpp b/include/proteus/core/mixins.hpp
new file mode 100644
index 000000000..4f8d6078f
--- /dev/null
+++ b/include/proteus/core/mixins.hpp
@@ -0,0 +1,19 @@
+#ifndef GUARD_PROTEUS_CORE_MIXINS
+#define GUARD_PROTEUS_CORE_MIXINS
+
+#include <cstddef>  // for std::byte, size_t
+
+namespace proteus {
+
+class Serializable {
+ public:
+  virtual ~Serializable() = default;
+
+  virtual size_t serializeSize() const = 0;
+  virtual void serialize(std::byte* data_out) const = 0;
+  virtual void deserialize(const std::byte* data_in) = 0;
+};
+
+}  // namespace proteus
+
+#endif  // GUARD_PROTEUS_CORE_MIXINS
diff --git a/include/proteus/core/predict_api.hpp b/include/proteus/core/predict_api.hpp
index 4f41d9c76..a0f91d388 100644
--- a/include/proteus/core/predict_api.hpp
+++ b/include/proteus/core/predict_api.hpp
@@ -35,9 +35,10 @@
 #include <variant>           // for operator!=, operator<
 #include <vector>            // for vector
 
-#include "proteus/build_options.hpp"         // for PROTEUS_ENABLE_TRACING
-#include "proteus/core/data_types.hpp"       // for DataType, mapTypeToStr
-#include "proteus/helpers/declarations.hpp"  // for InferenceResponseOutput
+#include "proteus/build_options.hpp"    // for PROTEUS_ENABLE_TRACING
+#include "proteus/core/data_types.hpp"  // for DataType, mapTypeToStr
+#include "proteus/core/mixins.hpp"      // for Serializable
+#include "proteus/declarations.hpp"     // for InferenceResponseOutput
 
 namespace proteus {
 
@@ -49,7 +50,7 @@ using Parameter = std::variant<bool, int32_t, double, std::string>;
  * bool, number or string). We further restrict numbers to be doubles or int32.
  *
  */
-class RequestParameters {
+class RequestParameters : public Serializable {
  public:
   /**
    * @brief Put in a key-value pair
@@ -127,6 +128,10 @@ class RequestParameters {
   auto end() { return parameters_.end(); }
   auto cend() const { return parameters_.cend(); }
 
+  size_t serializeSize() const override;
+  void serialize(std::byte *data_out) const override;
+  void deserialize(const std::byte *data_in) override;
+
   /// Provide an implementation to print the class with std::cout to an ostream
   friend std::ostream &operator<<(std::ostream &os,
                                   RequestParameters const &self) {
@@ -158,7 +163,7 @@ struct ServerMetadata {
 /**
  * @brief Holds an inference request's input data
  */
-class InferenceRequestInput {
+class InferenceRequestInput : public Serializable {
  public:
   /// Construct a new InferenceRequestInput object
   InferenceRequestInput();
@@ -177,7 +182,7 @@ class InferenceRequestInput {
   /// Set the request's data
   void setData(void *buffer);
   /// Set the request's shared data
-  void setData(std::shared_ptr<std::byte> buffer);
+  void setData(std::vector<std::byte> &&buffer);
   /// check if the stored data is shared
   bool sharedData() const;
 
@@ -217,6 +222,10 @@ class InferenceRequestInput {
   /// Get the tensor's size (number of elements)
   size_t getSize() const;
 
+  size_t serializeSize() const override;
+  void serialize(std::byte *data_out) const override;
+  void deserialize(const std::byte *data_in) override;
+
   /// Provide an implementation to print the class with std::cout to an ostream
   friend std::ostream &operator<<(std::ostream &os,
                                   InferenceRequestInput const &my_class) {
@@ -242,7 +251,7 @@ class InferenceRequestInput {
   DataType dataType_;
   RequestParametersPtr parameters_;
   void *data_;
-  std::shared_ptr<std::byte> shared_data_;
+  std::vector<std::byte> shared_data_;
 
   template <typename U>
   friend class InferenceRequestInputBuilder;
diff --git a/include/proteus/helpers/declarations.hpp b/include/proteus/declarations.hpp
similarity index 91%
rename from include/proteus/helpers/declarations.hpp
rename to include/proteus/declarations.hpp
index 3502a6569..95e906b3e 100644
--- a/include/proteus/helpers/declarations.hpp
+++ b/include/proteus/declarations.hpp
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef GUARD_PROTEUS_HELPERS_DECLARATIONS
-#define GUARD_PROTEUS_HELPERS_DECLARATIONS
+#ifndef GUARD_PROTEUS_DECLARATIONS
+#define GUARD_PROTEUS_DECLARATIONS
 
 #include <future>
 #include <memory>
@@ -52,4 +52,4 @@ using Kernels = std::unordered_map<std::string, int>;
 
 }  // namespace proteus
 
-#endif  // GUARD_PROTEUS_HELPERS_DECLARATIONS
+#endif  // GUARD_PROTEUS_DECLARATIONS
diff --git a/include/proteus/proteus.hpp b/include/proteus/proteus.hpp
index 7eb76c946..16d835590 100644
--- a/include/proteus/proteus.hpp
+++ b/include/proteus/proteus.hpp
@@ -23,7 +23,7 @@
 #include "proteus/core/data_types.hpp"
 #include "proteus/core/exceptions.hpp"
 #include "proteus/core/predict_api.hpp"
-#include "proteus/helpers/declarations.hpp"
+#include "proteus/declarations.hpp"
 #include "proteus/servers/server.hpp"
 // IWYU pragma: end_exports
 
diff --git a/proteus b/proteus
index 7a15ab5a5..694d74028 100755
--- a/proteus
+++ b/proteus
@@ -183,6 +183,10 @@ class WriteComposeFile:
         if ssh.exists():
             configs.append(f"{str(ssh)}:/home/proteus-user/.ssh:ro")
 
+        gpg = Path.home() / ".gnupg"
+        if gpg.exists():
+            configs.append(f"{str(gpg)}:/home/proteus-user/.gnupg:rw")
+
         return configs
 
     @staticmethod
diff --git a/src/proteus/CMakeLists.txt b/src/proteus/CMakeLists.txt
index 5c0141d88..0674501c9 100644
--- a/src/proteus/CMakeLists.txt
+++ b/src/proteus/CMakeLists.txt
@@ -19,17 +19,18 @@ enable_ipo_on_target(proteus-server-exe)
 set_target_properties(proteus-server-exe PROPERTIES OUTPUT_NAME proteus-server)
 
 target_link_libraries(
-  proteus-server-exe PRIVATE batching clients core helpers observation servers
+  proteus-server-exe PRIVATE batching clients core observation util servers
 )
 
 add_subdirectory(batching)
 add_subdirectory(bindings)
 add_subdirectory(buffers)
+add_subdirectory(client_operators)
 add_subdirectory(clients)
 add_subdirectory(core)
-add_subdirectory(helpers)
 add_subdirectory(observation)
 add_subdirectory(servers)
+add_subdirectory(util)
 add_subdirectory(workers)
 
 # Build and install the proteus library. The dual static/shared build process is
@@ -47,6 +48,7 @@ add_library(
   proteus
   ${TYPE}
   ${BATCHING_OBJS}
+  ${CLIENT_OPERATORS_OBJS}
   ${CLIENT_OBJS}
   ${CORE_OBJS}
   ${HELPER_OBJS}
@@ -54,7 +56,7 @@ add_library(
   ${SERVER_OBJS}
 )
 target_link_libraries(
-  proteus PRIVATE batching clients core helpers observation servers
+  proteus PRIVATE batching clients core observation util servers
 )
 
 add_library(
@@ -62,13 +64,14 @@ add_library(
                  ${OBSERVATION_OBJS} ${SERVER_OBJS}
 )
 target_link_libraries(
-  proteus-server PRIVATE batching core helpers observation servers
+  proteus-server PRIVATE batching core observation util servers
 )
 
 add_library(
-  proteus-client ${TYPE} ${CLIENT_OBJS} ${CORE_OBJS} ${OBSERVATION_OBJS}
+  proteus-client ${TYPE} ${CLIENT_OPERATORS_OBJS} ${CLIENT_OBJS} ${CORE_OBJS}
+                 ${OBSERVATION_OBJS}
 )
-target_link_libraries(proteus-client PRIVATE clients core observation)
+target_link_libraries(proteus-client PRIVATE clients core observation util)
 
 set(output_libraries proteus proteus-server proteus-client)
 foreach(lib ${output_libraries})
@@ -87,7 +90,9 @@ endforeach()
 # include(GenerateExportHeader)
 # generate_export_header(proteus EXPORT_FILE_NAME include/proteus/export.h)
 # target_compile_definitions(
-#   proteus PUBLIC "$<$<NOT:$<BOOL:${PROTEUS_BUILD_SHARED}>>:PROTEUS_STATIC_DEFINE>")
+#   proteus PUBLIC
+#   "$<$<NOT:$<BOOL:${PROTEUS_BUILD_SHARED}>>:PROTEUS_STATIC_DEFINE>"
+# )
 # target_include_directories(
 #   proteus PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/include>"
 # )
@@ -102,7 +107,7 @@ set(proteus_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/proteus"
 
 # cmake-format: off
 install(
-  TARGETS proteus proteus-client proteus-server
+  TARGETS proteus proteus-client proteus-server util
   EXPORT proteus_Targets
   RUNTIME COMPONENT proteus_Runtime
   LIBRARY COMPONENT proteus_Runtime
diff --git a/src/proteus/batching/batcher.hpp b/src/proteus/batching/batcher.hpp
index cb5e65ca2..80b6519f8 100644
--- a/src/proteus/batching/batcher.hpp
+++ b/src/proteus/batching/batcher.hpp
@@ -27,12 +27,12 @@
 #include <thread>   // for thread
 #include <vector>   // for vector
 
-#include "proteus/build_options.hpp"         // for PROTEUS_ENABLE_LOGGING
-#include "proteus/core/predict_api.hpp"      // for RequestParameters
-#include "proteus/helpers/declarations.hpp"  // for BufferPtrs, InferenceReq...
-#include "proteus/helpers/queue.hpp"         // for BlockingConcurrentQueue
-#include "proteus/observation/logging.hpp"   // for LoggerPtr
-#include "proteus/observation/tracing.hpp"   // for TracePtr
+#include "proteus/build_options.hpp"        // for PROTEUS_ENABLE_LOGGING
+#include "proteus/core/predict_api.hpp"     // for RequestParameters
+#include "proteus/declarations.hpp"         // for BufferPtrs, InferenceReq...
+#include "proteus/observation/logging.hpp"  // for LoggerPtr
+#include "proteus/observation/tracing.hpp"  // for TracePtr
+#include "proteus/util/queue.hpp"           // for BlockingConcurrentQueue
 
 namespace proteus {
 class Buffer;
diff --git a/src/proteus/batching/hard.cpp b/src/proteus/batching/hard.cpp
index c9afe9819..2fe611d9f 100644
--- a/src/proteus/batching/hard.cpp
+++ b/src/proteus/batching/hard.cpp
@@ -25,14 +25,14 @@
 #include <utility>  // for move
 #include <vector>   // for vector
 
-#include "proteus/build_options.hpp"         // for PROTEUS_ENABLE_METRICS
-#include "proteus/core/exceptions.hpp"       // for invalid_argument
-#include "proteus/core/interface.hpp"        // for Interface
-#include "proteus/helpers/declarations.hpp"  // for InterfacePtr
-#include "proteus/helpers/queue.hpp"         // for BlockingConcurrentQueue
-#include "proteus/helpers/thread.hpp"        // for setThreadName
-#include "proteus/observation/metrics.hpp"   // for Metrics, MetricCounterIDs
-#include "proteus/observation/tracing.hpp"   // for Trace
+#include "proteus/build_options.hpp"        // for PROTEUS_ENABLE_METRICS
+#include "proteus/core/exceptions.hpp"      // for invalid_argument
+#include "proteus/core/interface.hpp"       // for Interface
+#include "proteus/declarations.hpp"         // for InterfacePtr
+#include "proteus/observation/metrics.hpp"  // for Metrics, MetricCounterIDs
+#include "proteus/observation/tracing.hpp"  // for Trace
+#include "proteus/util/queue.hpp"           // for BlockingConcurrentQueue
+#include "proteus/util/thread.hpp"          // for setThreadName
 
 // IWYU pragma: no_forward_declare proteus::Buffer
 
@@ -40,7 +40,7 @@ namespace proteus {
 
 void HardBatcher::doRun(WorkerInfo* worker) {
   auto thread_name = "batch" + this->getName();
-  setThreadName(thread_name);
+  util::setThreadName(thread_name);
   InterfacePtr req;
   bool run = true;
 
diff --git a/src/proteus/batching/soft.cpp b/src/proteus/batching/soft.cpp
index 362e9427b..bc4b924c5 100644
--- a/src/proteus/batching/soft.cpp
+++ b/src/proteus/batching/soft.cpp
@@ -29,16 +29,16 @@
 #include <utility>    // for move
 #include <vector>     // for vector
 
-#include "proteus/build_options.hpp"         // for PROTEUS_ENABLE_METRICS
-#include "proteus/core/exceptions.hpp"       // for invalid_argument
-#include "proteus/core/interface.hpp"        // for Interface
-#include "proteus/core/predict_api.hpp"      // for RequestParameters
-#include "proteus/helpers/declarations.hpp"  // for InterfacePtr
-#include "proteus/helpers/queue.hpp"         // for BlockingConcurrentQueue
-#include "proteus/helpers/thread.hpp"        // for setThreadName
-#include "proteus/observation/logging.hpp"   // for Logger, PROTEUS_LOG_DEBUG
-#include "proteus/observation/metrics.hpp"   // for Metrics, MetricCounterIDs
-#include "proteus/observation/tracing.hpp"   // for Trace
+#include "proteus/build_options.hpp"        // for PROTEUS_ENABLE_METRICS
+#include "proteus/core/exceptions.hpp"      // for invalid_argument
+#include "proteus/core/interface.hpp"       // for Interface
+#include "proteus/core/predict_api.hpp"     // for RequestParameters
+#include "proteus/declarations.hpp"         // for InterfacePtr
+#include "proteus/observation/logging.hpp"  // for Logger, PROTEUS_LOG_DEBUG
+#include "proteus/observation/metrics.hpp"  // for Metrics, MetricCounterIDs
+#include "proteus/observation/tracing.hpp"  // for Trace
+#include "proteus/util/queue.hpp"           // for BlockingConcurrentQueue
+#include "proteus/util/thread.hpp"          // for setThreadName
 
 using std::chrono::duration_cast;
 using std::chrono::milliseconds;
@@ -49,7 +49,7 @@ namespace proteus {
 
 void SoftBatcher::doRun(WorkerInfo* worker) {
   auto thread_name = "batch" + this->getName();
-  setThreadName(thread_name);
+  util::setThreadName(thread_name);
 #ifdef PROTEUS_ENABLE_LOGGING
   const auto& logger = this->getLogger();
 #endif
diff --git a/src/proteus/bindings/python/CMakeLists.txt b/src/proteus/bindings/python/CMakeLists.txt
index f9095980d..932263163 100644
--- a/src/proteus/bindings/python/CMakeLists.txt
+++ b/src/proteus/bindings/python/CMakeLists.txt
@@ -43,6 +43,7 @@ add_custom_target(
 )
 
 add_subdirectory(core) # sets CORE_OBJS
+add_subdirectory(client_operators) # sets CLIENT_OPERATORS_OBJS
 add_subdirectory(clients) # sets CLIENT_OBJS
 add_subdirectory(servers) # sets SERVER_OBJS
 
@@ -63,8 +64,8 @@ endif()
 pybind11_add_module(proteus_py MODULE proteus.cpp ${dummy_new_file})
 target_include_directories(proteus_py PRIVATE ${PROTEUS_INCLUDE_DIRS})
 target_link_libraries(
-  proteus_py PRIVATE proteus ${Python_LIBRARIES} ${CORE_OBJS} ${CLIENT_OBJS}
-                     ${SERVER_OBJS}
+  proteus_py PRIVATE proteus ${Python_LIBRARIES} ${CORE_OBJS}
+                     ${CLIENT_OPERATORS_OBJS} ${CLIENT_OBJS} ${SERVER_OBJS}
 )
 set_target_properties(proteus_py PROPERTIES OUTPUT_NAME _proteus)
 
@@ -89,9 +90,9 @@ if(${Pybind11Stubgen_FOUND})
     POST_BUILD
     COMMAND PYTHONPATH=${output_directory} pybind11-stubgen -o
             ${output_directory} _proteus --no-setup-py
-    COMMAND mkdir -p ${output_directory}/proteus
+    COMMAND mkdir -p ${output_directory}/src/proteus
     COMMAND cp -rf ${output_directory}/_proteus-stubs/*
-            ${output_directory}/proteus
+            ${output_directory}/src/proteus
     COMMAND rm -rf ${output_directory}/_proteus-stubs
   )
 else()
@@ -101,29 +102,11 @@ else()
 endif()
 
 # copy over Python package source files to the build tree
-configure_file(
-  ${CMAKE_CURRENT_SOURCE_DIR}/setup.py ${CMAKE_CURRENT_BINARY_DIR}/setup.py
-  COPYONLY
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/setup.py
+     DESTINATION ${CMAKE_CURRENT_BINARY_DIR}
 )
-configure_file(
-  ${CMAKE_CURRENT_SOURCE_DIR}/src/proteus/__init__.py
-  ${CMAKE_CURRENT_BINARY_DIR}/src/proteus/__init__.py COPYONLY
-)
-configure_file(
-  ${CMAKE_CURRENT_SOURCE_DIR}/src/proteus/clients/__init__.py
-  ${CMAKE_CURRENT_BINARY_DIR}/src/proteus/clients/__init__.py COPYONLY
-)
-configure_file(
-  ${CMAKE_CURRENT_SOURCE_DIR}/src/proteus/predict_api/__init__.py
-  ${CMAKE_CURRENT_BINARY_DIR}/src/proteus/predict_api/__init__.py COPYONLY
-)
-configure_file(
-  ${CMAKE_CURRENT_SOURCE_DIR}/src/proteus/servers/__init__.py
-  ${CMAKE_CURRENT_BINARY_DIR}/src/proteus/servers/__init__.py COPYONLY
-)
-configure_file(
-  ${CMAKE_CURRENT_SOURCE_DIR}/src/proteus/py.typed
-  ${CMAKE_CURRENT_BINARY_DIR}/src/proteus/py.typed COPYONLY
+file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/src
+     DESTINATION ${CMAKE_CURRENT_BINARY_DIR}
 )
 
 get_target_property(output_directory_2 proteus_py LIBRARY_OUTPUT_DIRECTORY)
@@ -138,7 +121,7 @@ if(NOT output_directory_2)
   add_custom_command(
     TARGET proteus_py
     POST_BUILD
-    COMMAND pip install --prefix ~/.local .
+    COMMAND pip install --prefix ~/.local ${CMAKE_CURRENT_BINARY_DIR}
     COMMAND cp ${CMAKE_CURRENT_BINARY_DIR}/*.so ${PY_USER_SITE_PACKAGES}
   )
 endif()
diff --git a/src/proteus/bindings/python/client_operators/CMakeLists.txt b/src/proteus/bindings/python/client_operators/CMakeLists.txt
new file mode 100644
index 000000000..df0ac5779
--- /dev/null
+++ b/src/proteus/bindings/python/client_operators/CMakeLists.txt
@@ -0,0 +1,34 @@
+# Copyright 2022 Xilinx Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+list(APPEND TARGETS infer_async)
+
+list(APPEND LINK_TARGETS) # create empty list
+
+foreach(target ${TARGETS})
+  add_library(${target}_py OBJECT ${target}.cpp)
+  target_include_directories(
+    ${target}_py PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/.. ${PROTEUS_INCLUDE_DIRS}
+                         ${Python_INCLUDE_DIRS}
+  )
+  target_link_libraries(
+    ${target}_py PRIVATE ${Python_LIBRARIES} ${target}
+                         $<TARGET_OBJECTS:${target}>
+  )
+  add_dependencies(${target}_py pybind11Mkdoc)
+
+  list(APPEND LINK_TARGETS ${target}_py)
+endforeach()
+
+set(CLIENT_OPERATORS_OBJS ${LINK_TARGETS} PARENT_SCOPE)
diff --git a/src/proteus/bindings/python/client_operators/client_operators.hpp b/src/proteus/bindings/python/client_operators/client_operators.hpp
new file mode 100644
index 000000000..abfeb0c74
--- /dev/null
+++ b/src/proteus/bindings/python/client_operators/client_operators.hpp
@@ -0,0 +1,33 @@
+// Copyright 2022 Xilinx Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @file
+ * @brief Defines the objects for Python bindings
+ */
+
+#ifndef GUARD_PROTEUS_BINDINGS_PYTHON_CLIENT_OPERATORS_CLIENT_OPERATORS
+#define GUARD_PROTEUS_BINDINGS_PYTHON_CLIENT_OPERATORS_CLIENT_OPERATORS
+
+namespace pybind11 {
+class module_;
+}
+
+namespace proteus {
+
+void wrapInferAsync(pybind11::module_ &);
+
+}  // namespace proteus
+
+#endif  // GUARD_PROTEUS_BINDINGS_PYTHON_CLIENT_OPERATORS_CLIENT_OPERATORS
diff --git a/src/proteus/bindings/python/client_operators/infer_async.cpp b/src/proteus/bindings/python/client_operators/infer_async.cpp
new file mode 100644
index 000000000..e0f058296
--- /dev/null
+++ b/src/proteus/bindings/python/client_operators/infer_async.cpp
@@ -0,0 +1,42 @@
+// Copyright 2022 Xilinx Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @file
+ * @brief Implements the Python bindings for the http.hpp header
+ */
+
+#include "proteus/client_operators/infer_async.hpp"
+
+#include <pybind11/cast.h>      // for arg
+#include <pybind11/pybind11.h>  // for class_, init
+#include <pybind11/stl.h>       // IWYU pragma: keep
+
+#include <unordered_map>  // for unordered_map
+
+#include "proteus/bindings/python/helpers/docstrings.hpp"  // for DOCS
+
+namespace py = pybind11;
+
+namespace proteus {
+
+void wrapInferAsync(py::module_ &m) {
+  m.def("inferAsyncOrdered", &inferAsyncOrdered, py::arg("client"),
+        py::arg("model"), py::arg("requests"));
+  m.def("inferAsyncOrderedBatched", &inferAsyncOrderedBatched,
+        py::arg("client"), py::arg("model"), py::arg("requests"),
+        py::arg("batch_sizes"));
+}
+
+}  // namespace proteus
diff --git a/src/proteus/bindings/python/clients/http.cpp b/src/proteus/bindings/python/clients/http.cpp
index 11f5af2d5..ad21bd4c4 100644
--- a/src/proteus/bindings/python/clients/http.cpp
+++ b/src/proteus/bindings/python/clients/http.cpp
@@ -26,7 +26,7 @@
 #include <unordered_map>  // for unordered_map
 
 #include "proteus/bindings/python/helpers/docstrings.hpp"  // for DOCS
-#include "proteus/helpers/declarations.hpp"
+#include "proteus/declarations.hpp"
 
 namespace py = pybind11;
 
diff --git a/src/proteus/bindings/python/core/predict_api.cpp b/src/proteus/bindings/python/core/predict_api.cpp
index 89cb5448b..577da6604 100644
--- a/src/proteus/bindings/python/core/predict_api.cpp
+++ b/src/proteus/bindings/python/core/predict_api.cpp
@@ -171,13 +171,8 @@ void wrapRequestParameters(py::module_ &m) {
 
 template <typename T>
 py::array_t<T> getData(const proteus::InferenceRequestInput &self) {
-  if (self.sharedData()) {
-    auto *data = static_cast<std::vector<T> *>(self.getData());
-    return py::array_t<T>(self.getSize(), data->data());
-  } else {
-    auto *data = static_cast<T *>(self.getData());
-    return py::array_t<T>(self.getSize(), data);
-  }
+  auto *data = static_cast<T *>(self.getData());
+  return py::array_t<T>(self.getSize(), data);
 
   // return py::memoryview::from_memory(data->data(), self.getSize());
 }
@@ -225,10 +220,10 @@ void wrapPredictApi(py::module_ &m) {
     .def(
       "setStringData",
       [](proteus::InferenceRequestInput &self, std::string &str) {
-        auto ptr = std::make_shared<std::string>(str);
-        auto ptr_cast = std::reinterpret_pointer_cast<std::byte>(ptr);
-        // self.setData(static_cast<void *>(&str));
-        self.setData(ptr_cast);
+        std::vector<std::byte> data;
+        data.resize(str.length());
+        memcpy(data.data(), str.data(), str.length());
+        self.setData(std::move(data));
       },
       py::keep_alive<1, 2>())
     .def("getUint8Data", &getData<uint8_t>, py::keep_alive<0, 1>())
@@ -241,15 +236,7 @@ void wrapPredictApi(py::module_ &m) {
     .def("getInt64Data", &getData<int64_t>, py::keep_alive<0, 1>())
     .def("getFp32Data", &getData<float>, py::keep_alive<0, 1>())
     .def("getFp64Data", &getData<double>, py::keep_alive<0, 1>())
-
-    // pybind11 complains about weak reference if I add keep_alive<0,1> to this
-    .def(
-      "getStringData",
-      [](proteus::InferenceRequestInput &self) {
-        auto *data = static_cast<std::string *>(self.getData());
-        return *data;
-      },
-      py::return_value_policy::reference)
+    .def("getStringData", &getData<char>, py::keep_alive<0, 1>())
     .def_property("name", &InferenceRequestInput::getName,
                   &InferenceRequestInput::setName)
     .def_property("shape", &InferenceRequestInput::getShape, setShape)
diff --git a/src/proteus/bindings/python/proteus.cpp b/src/proteus/bindings/python/proteus.cpp
index 05bf82b12..72744e713 100644
--- a/src/proteus/bindings/python/proteus.cpp
+++ b/src/proteus/bindings/python/proteus.cpp
@@ -19,6 +19,7 @@
 
 #include <pybind11/pybind11.h>
 
+#include "proteus/bindings/python/client_operators/client_operators.hpp"
 #include "proteus/bindings/python/clients/client.hpp"
 #include "proteus/bindings/python/core/data_types.hpp"
 #include "proteus/bindings/python/core/predict_api.hpp"
@@ -31,6 +32,8 @@ namespace proteus {
 
 PYBIND11_MODULE(_proteus, m) {
   py::module n = m.def_submodule("predict_api", "predict_api documentation");
+  py::module f =
+    m.def_submodule("client_operators", "client operators documentation");
   py::module c = m.def_submodule("clients", "client documentation");
   py::module s = m.def_submodule("servers", "server documentation");
   m.doc() = "proteus inference library";
@@ -45,6 +48,7 @@ PYBIND11_MODULE(_proteus, m) {
   wrapHttpClient(c);
   wrapWebSocketClient(c);
   wrapServer(s);
+  wrapInferAsync(f);
 }
 
 }  // namespace proteus
diff --git a/src/proteus/bindings/python/setup.py b/src/proteus/bindings/python/setup.py
index cd690d763..0f942ccef 100644
--- a/src/proteus/bindings/python/setup.py
+++ b/src/proteus/bindings/python/setup.py
@@ -37,9 +37,7 @@
     package_dir={"": "src"},
     package_data={
         "": [
-            "_proteus.cpython-36m-x86_64-linux-gnu.so",
-            "proteus-stubs/*.pyi",
-            "_proteus-stubs/*.pyi",
+            "*.pyi",
         ]
     },
     zip_safe=False,  # required for mypy
diff --git a/src/proteus/bindings/python/src/proteus/__init__.py b/src/proteus/bindings/python/src/proteus/__init__.py
index b32d94752..02c20d83e 100644
--- a/src/proteus/bindings/python/src/proteus/__init__.py
+++ b/src/proteus/bindings/python/src/proteus/__init__.py
@@ -12,6 +12,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import functools
+import multiprocessing as mp
+import sys
+import time
+
 from _proteus import *
 
 
@@ -88,3 +93,62 @@ def ImageInferenceRequest(images, asTensor=False):
             raise TypeError("Unknown type passed to ImageInferenceRequest")
         request.addInputTensor(input_n)
     return request
+
+
+def _infer(client, model, image):
+    """
+    Make an inference. For multiprocessing, the function must be defined at the
+    top level
+
+    Args:
+        client (proteus.client): client to send request with
+        model (str): name of the model/worker to make the inference
+        image (np.array): Image to send to the server
+    """
+    request = ImageInferenceRequest(image)
+    response = client.modelInfer(model, request)
+    assert not response.isError(), response.getError()
+
+
+def parallel_infer(client, model, data, processes):
+    """
+    Make an inference to the server in parallel with n processes
+
+    Args:
+        client (proteus.client): Client to make the inference with
+        model (str): Name of the model/worker to make the inference
+        data (list[np.ndarray]): List of data to send
+        processes (int): number of processes to use
+
+    Returns:
+        list[proteus.InferenceResponse]: Responses for each request
+    """
+
+    make_inference = functools.partial(_infer, client, model)
+    with mp.Pool(processes) as p:
+        p.map(make_inference, data)
+        p.close()
+        p.join()
+
+
+def start_http_client_server(address: str, extension=None):
+    client = clients.HttpClient(address)
+    port = address.split(":")[-1]
+
+    # if it's not already started, start it here
+    start_server = not client.serverLive()
+    if start_server:
+        server = servers.Server()
+        server.startHttp(int(port))
+        while not client.serverLive():
+            time.sleep(1)
+    else:
+        server = None
+
+    if extension:
+        metadata = client.serverMetadata()
+        if extension not in metadata.extensions:
+            print(f"{extension} support required but not found.")
+            sys.exit(0)
+
+    return client, server
diff --git a/src/proteus/bindings/python/src/proteus/client_operators/__init__.py b/src/proteus/bindings/python/src/proteus/client_operators/__init__.py
new file mode 100644
index 000000000..e53976c0f
--- /dev/null
+++ b/src/proteus/bindings/python/src/proteus/client_operators/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2022 Xilinx Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from _proteus.client_operators import *
diff --git a/src/proteus/buffers/buffer.cpp b/src/proteus/buffers/buffer.cpp
index 3b757447c..49c437ede 100644
--- a/src/proteus/buffers/buffer.cpp
+++ b/src/proteus/buffers/buffer.cpp
@@ -32,78 +32,4 @@ size_t Buffer::write(void* data, size_t offset, size_t size) {
   return offset + size;
 }
 
-size_t Buffer::write(bool value, size_t offset) {
-  constexpr auto bytes = DataType("BOOL").size();
-  std::memcpy(this->data(offset), &value, bytes);
-  return offset + bytes;
-}
-size_t Buffer::write(uint8_t value, size_t offset) {
-  constexpr auto bytes = DataType("UINT8").size();
-  std::memcpy(this->data(offset), &value, bytes);
-  return offset + bytes;
-}
-size_t Buffer::write(uint16_t value, size_t offset) {
-  constexpr auto bytes = DataType("UINT16").size();
-  std::memcpy(this->data(offset), &value, bytes);
-  return offset + bytes;
-}
-size_t Buffer::write(uint32_t value, size_t offset) {
-  constexpr auto bytes = DataType("UINT32").size();
-  std::memcpy(this->data(offset), &value, bytes);
-  return offset + bytes;
-}
-size_t Buffer::write(uint64_t value, size_t offset) {
-  constexpr auto bytes = DataType("UINT64").size();
-  std::memcpy(this->data(offset), &value, bytes);
-  return offset + bytes;
-}
-size_t Buffer::write(int8_t value, size_t offset) {
-  constexpr auto bytes = DataType("INT8").size();
-  std::memcpy(this->data(offset), &value, bytes);
-  return offset + bytes;
-}
-size_t Buffer::write(int16_t value, size_t offset) {
-  constexpr auto bytes = DataType("INT16").size();
-  std::memcpy(this->data(offset), &value, bytes);
-  return offset + bytes;
-}
-size_t Buffer::write(int32_t value, size_t offset) {
-  constexpr auto bytes = DataType("INT32").size();
-  std::memcpy(this->data(offset), &value, bytes);
-  return offset + bytes;
-}
-size_t Buffer::write(int64_t value, size_t offset) {
-  constexpr auto bytes = DataType("INT64").size();
-  std::memcpy(this->data(offset), &value, bytes);
-  return offset + bytes;
-}
-size_t Buffer::write(float value, size_t offset) {
-  constexpr auto bytes = DataType("FP32").size();
-  std::memcpy(this->data(offset), &value, bytes);
-  return offset + bytes;
-}
-size_t Buffer::write(double value, size_t offset) {
-  constexpr auto bytes = DataType("FP64").size();
-  std::memcpy(this->data(offset), &value, bytes);
-  return offset + bytes;
-}
-// not quite sure what the best way to do this is. The commented out code is
-// other attempts. This works but may be non-optimal.
-size_t Buffer::write(std::string value, size_t offset) {
-  // constexpr auto bytes = types::getSize(DataType::STRING);
-  // std::memcpy(this->data(offset), &value, bytes);
-
-  char null_term = '\0';
-  std::copy(value.begin(), value.end(),
-            reinterpret_cast<char*>(this->data(offset)));
-  // strcpy(reinterpret_cast<char*>(static_cast<std::byte*>(this->data()) +
-  // this->write_counter_),
-  // value.c_str());
-  std::memcpy(static_cast<std::byte*>(this->data(offset)) + value.length(),
-              &null_term, 1);
-
-  // this->write_counter_ += value.length() + 1;
-  return offset + value.length() + 1;
-}
-
 }  // namespace proteus
diff --git a/src/proteus/buffers/buffer.hpp b/src/proteus/buffers/buffer.hpp
index 78122c15c..25b1fee6f 100644
--- a/src/proteus/buffers/buffer.hpp
+++ b/src/proteus/buffers/buffer.hpp
@@ -23,20 +23,17 @@
 
 #include <cstddef>  // for size_t
 #include <cstdint>  // for int16_t, int32_t, int64_t, int8_t, uint16_t, uint...
+#include <cstring>  // for memcpy, size_t
 #include <string>   // for string
 
 namespace proteus {
 
-enum class Status;
-
 /**
  * @brief The base buffer class used in Proteus. Buffer implementations should
  * extend this class and override the methods.
  */
 class Buffer {
  public:
-  /// Construct a new Proteus Buffer object
-  Buffer() {}
   /// Destroy the Proteus Buffer object
   virtual ~Buffer() = default;
 
@@ -72,183 +69,27 @@ class Buffer {
    *
    * @param value value to write
    */
-  virtual size_t write(bool value, size_t offset);
-
-  /**
-   * @brief Write a uint8_t value to the buffer
-   *
-   * @param value value to write
-   */
-  virtual size_t write(uint8_t value, size_t offset);
-
-  /**
-   * @brief Write a uint16_t value to the buffer
-   *
-   * @param value value to write
-   */
-  virtual size_t write(uint16_t value, size_t offset);
-
-  /**
-   * @brief Write a uint32_t value to the buffer
-   *
-   * @param value value to write
-   */
-  virtual size_t write(uint32_t value, size_t offset);
-
-  /**
-   * @brief Write a uint64_t value to the buffer
-   *
-   * @param value value to write
-   */
-  virtual size_t write(uint64_t value, size_t offset);
-
-  /**
-   * @brief Write a int8_t value to the buffer
-   *
-   * @param value value to write
-   */
-  virtual size_t write(int8_t value, size_t offset);
-
-  /**
-   * @brief Write a int16_t value to the buffer
-   *
-   * @param value value to write
-   */
-  virtual size_t write(int16_t value, size_t offset);
-
-  /**
-   * @brief Write a int32_t value to the buffer
-   *
-   * @param value value to write
-   */
-  virtual size_t write(int32_t value, size_t offset);
-
-  /**
-   * @brief Write a int64_t value to the buffer
-   *
-   * @param value value to write
-   */
-  virtual size_t write(int64_t value, size_t offset);
-
-  /**
-   * @brief Write a float value to the buffer
-   *
-   * @param value value to write
-   */
-  virtual size_t write(float value, size_t offset);
-
-  /**
-   * @brief Write a double value to the buffer
-   *
-   * @param value value to write
-   */
-  virtual size_t write(double value, size_t offset);
-
-  /**
-   * @brief Write a string value to the buffer
-   *
-   * @param value value to write
-   */
-  virtual size_t write(std::string value, size_t offset);
-
-  // /**
-  //  * @brief Read a bool value from the buffer
-  //  *
-  //  * @param index address to read from
-  //  * @param value holds the read value if read is successful
-  //  * @return Status::kOK if successful
-  //  */
-  // virtual Status read(int index, bool& value) = 0;
-
-  // /**
-  //  * @brief Read a uint8_t value from the buffer
-  //  *
-  //  * @param index address to read from
-  //  * @param value holds the read value if read is successful
-  //  * @return Status::kOK if successful
-  //  */
-  // virtual Status read(int index, uint8_t& value) = 0;
-
-  // /**
-  //  * @brief Read a uint16_t value from the buffer
-  //  *
-  //  * @param index address to read from
-  //  * @param value holds the read value if read is successful
-  //  * @return Status::kOK if successful
-  //  */
-  // virtual Status read(int index, uint16_t& value) = 0;
-
-  // /**
-  //  * @brief Read a uint32_t value from the buffer
-  //  *
-  //  * @param index address to read from
-  //  * @param value holds the read value if read is successful
-  //  * @return Status::kOK if successful
-  //  */
-  // virtual Status read(int index, uint32_t& value) = 0;
-
-  // /**
-  //  * @brief Read a uint64_t value from the buffer
-  //  *
-  //  * @param index address to read from
-  //  * @param value holds the read value if read is successful
-  //  * @return Status::kOK if successful
-  //  */
-  // virtual Status read(int index, uint64_t& value) = 0;
-
-  // /**
-  //  * @brief Read a int8_t value from the buffer
-  //  *
-  //  * @param index address to read from
-  //  * @param value holds the read value if read is successful
-  //  * @return Status::kOK if successful
-  //  */
-  // virtual Status read(int index, int8_t& value) = 0;
-
-  // /**
-  //  * @brief Read a int16_t value from the buffer
-  //  *
-  //  * @param index address to read from
-  //  * @param value holds the read value if read is successful
-  //  * @return Status::kOK if successful
-  //  */
-  // virtual Status read(int index, int16_t& value) = 0;
-
-  // /**
-  //  * @brief Read a int32_t value from the buffer
-  //  *
-  //  * @param index address to read from
-  //  * @param value holds the read value if read is successful
-  //  * @return Status::kOK if successful
-  //  */
-  // virtual Status read(int index, int32_t& value) = 0;
-
-  // /**
-  //  * @brief Read a int64_t value from the buffer
-  //  *
-  //  * @param index address to read from
-  //  * @param value holds the read value if read is successful
-  //  * @return Status::kOK if successful
-  //  */
-  // virtual Status read(int index, int64_t& value) = 0;
-
-  // /**
-  //  * @brief Read a float value from the buffer
-  //  *
-  //  * @param index address to read from
-  //  * @param value holds the read value if read is successful
-  //  * @return Status::kOK if successful
-  //  */
-  // virtual Status read(int index, float& value) = 0;
-
-  // /**
-  //  * @brief Read a double value from the buffer
-  //  *
-  //  * @param index address to read from
-  //  * @param value holds the read value if read is successful
-  //  * @return Status::kOK if successful
-  //  */
-  // virtual Status read(int index, double& value) = 0;
+  template <typename T>
+  size_t write(T value, size_t offset) {
+    if constexpr (std::is_same_v<std::string, T>) {
+      // not quite sure what the best way to do this is. The commented out code
+      // is other attempts. This works but may be non-optimal.
+      char null_term = '\0';
+      std::copy(value.begin(), value.end(),
+                reinterpret_cast<char*>(this->data(offset)));
+      // strcpy(reinterpret_cast<char*>(static_cast<std::byte*>(this->data()) +
+      // this->write_counter_),
+      // value.c_str());
+      std::memcpy(static_cast<std::byte*>(this->data(offset)) + value.length(),
+                  &null_term, 1);
+
+      // this->write_counter_ += value.length() + 1;
+      return offset + value.length() + 1;
+    } else {
+      std::memcpy(this->data(offset), &value, sizeof(T));
+      return offset + sizeof(T);
+    }
+  }
 };
 
 }  // namespace proteus
diff --git a/src/proteus/buffers/vector_buffer.cpp b/src/proteus/buffers/vector_buffer.cpp
index 85523ecc8..7435e8f1b 100644
--- a/src/proteus/buffers/vector_buffer.cpp
+++ b/src/proteus/buffers/vector_buffer.cpp
@@ -23,7 +23,7 @@
 #include <memory>     // for make_unique
 #include <utility>    // for move
 
-#include "proteus/helpers/declarations.hpp"  // for BufferPtrs
+#include "proteus/declarations.hpp"  // for BufferPtrs
 
 namespace proteus {
 
diff --git a/src/proteus/buffers/vector_buffer.hpp b/src/proteus/buffers/vector_buffer.hpp
index ae65d6896..ff8be3066 100644
--- a/src/proteus/buffers/vector_buffer.hpp
+++ b/src/proteus/buffers/vector_buffer.hpp
@@ -25,7 +25,7 @@
 
 #include "proteus/buffers/buffer.hpp"   // for Buffer
 #include "proteus/core/data_types.hpp"  // for DataType
-#include "proteus/helpers/queue.hpp"    // for BufferPtrsQueue
+#include "proteus/util/queue.hpp"       // for BufferPtrsQueue
 
 namespace proteus {
 
diff --git a/src/proteus/client_operators/CMakeLists.txt b/src/proteus/client_operators/CMakeLists.txt
new file mode 100644
index 000000000..c874f9431
--- /dev/null
+++ b/src/proteus/client_operators/CMakeLists.txt
@@ -0,0 +1,30 @@
+# Copyright 2022 Xilinx Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+list(APPEND files infer_async)
+
+foreach(file ${files})
+  set(target ${file})
+  add_library(${target} OBJECT ${file}.cpp)
+  target_include_directories(${target} PRIVATE ${PROTEUS_INCLUDE_DIRS})
+  enable_ipo_on_target(${target})
+
+  list(APPEND CLIENT_OPERATORS_OBJS $<TARGET_OBJECTS:${target}>)
+  list(APPEND targets ${target})
+endforeach()
+
+add_library(client_operators STATIC ${CLIENT_OPERATORS_OBJS})
+target_link_libraries(client_operators INTERFACE ${targets})
+
+set(CLIENT_OPERATORS_OBJS ${CLIENT_OPERATORS_OBJS} PARENT_SCOPE)
diff --git a/src/proteus/client_operators/infer_async.cpp b/src/proteus/client_operators/infer_async.cpp
new file mode 100644
index 000000000..d0fc8edb0
--- /dev/null
+++ b/src/proteus/client_operators/infer_async.cpp
@@ -0,0 +1,77 @@
+// Copyright 2022 Xilinx Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "proteus/client_operators/infer_async.hpp"
+
+#include <queue>
+#include <thread>
+
+namespace proteus {
+
+std::vector<InferenceResponse> inferAsyncOrdered(
+  Client* client, const std::string& model,
+  const std::vector<InferenceRequest>& requests) {
+  std::queue<InferenceResponseFuture> q;
+  for (const auto& request : requests) {
+    q.push(client->modelInferAsync(model, request));
+  }
+
+  const auto num_requests = requests.size();
+  std::vector<InferenceResponse> responses;
+  responses.reserve(num_requests);
+  for (auto i = 0U; i < num_requests; ++i) {
+    auto& future = q.front();
+    responses.push_back(future.get());
+    q.pop();
+  }
+  return responses;
+}
+
+std::vector<InferenceResponse> inferAsyncOrderedBatched(
+  Client* client, const std::string& model,
+  const std::vector<InferenceRequest>& requests, size_t batch_size) {
+  auto num_requests = requests.size();
+  std::vector<InferenceResponse> responses;
+  responses.reserve(num_requests);
+  auto start_index = 0U;
+  std::queue<InferenceResponseFuture> q;
+
+  while (start_index + batch_size < num_requests) {
+    for (auto i = start_index; i < batch_size; ++i) {
+      q.push(client->modelInferAsync(model, requests[i]));
+    }
+
+    for (auto i = 0U; i < batch_size; ++i) {
+      auto& future = q.front();
+      responses.push_back(future.get());
+      q.pop();
+    }
+    start_index += batch_size;
+  }
+
+  if (start_index != num_requests) {
+    for (auto i = start_index; i < num_requests; ++i) {
+      q.push(client->modelInferAsync(model, requests[i]));
+    }
+
+    for (auto i = 0U; i < num_requests - start_index; ++i) {
+      auto& future = q.front();
+      responses.push_back(future.get());
+      q.pop();
+    }
+  }
+  return responses;
+}
+
+}  // namespace proteus
diff --git a/src/proteus/clients/CMakeLists.txt b/src/proteus/clients/CMakeLists.txt
index 17d54ed8b..b32899519 100644
--- a/src/proteus/clients/CMakeLists.txt
+++ b/src/proteus/clients/CMakeLists.txt
@@ -38,7 +38,7 @@ endforeach()
 target_link_libraries(native INTERFACE batching)
 
 if(${PROTEUS_ENABLE_HTTP})
-  target_link_libraries(http_internal INTERFACE helpers jsoncpp)
+  target_link_libraries(http_internal INTERFACE jsoncpp util)
   target_link_libraries(websocket INTERFACE Drogon::Drogon)
 endif()
 
diff --git a/src/proteus/clients/grpc.cpp b/src/proteus/clients/grpc.cpp
index 12e723a61..0cb25bf6d 100644
--- a/src/proteus/clients/grpc.cpp
+++ b/src/proteus/clients/grpc.cpp
@@ -36,11 +36,12 @@
 #include <variant>        // for visit
 #include <vector>         // for vector
 
-#include "predict_api.grpc.pb.h"             // for GRPCInferenceService...
-#include "predict_api.pb.h"                  // for RepeatedField, Infer...
-#include "proteus/core/data_types.hpp"       // for DataType, DataType::...
-#include "proteus/core/exceptions.hpp"       // for bad_status
-#include "proteus/helpers/declarations.hpp"  // for InferenceResponseOutput
+#include "predict_api.grpc.pb.h"  // for GRPCInferenceService...
+#include "predict_api.pb.h"       // for RepeatedField, Infer...
+#include "proteus/clients/grpc_internal.hpp"
+#include "proteus/core/data_types.hpp"  // for DataType, DataType::...
+#include "proteus/core/exceptions.hpp"  // for bad_status
+#include "proteus/declarations.hpp"     // for InferenceResponseOutput
 
 using grpc::ClientContext;
 using grpc::Status;
@@ -197,32 +198,6 @@ std::vector<std::string> GrpcClient::modelList() {
   throw bad_status(status.error_message());
 }
 
-// refer to cppreference for std::visit
-// helper type for the visitor #4
-template <class... Ts>
-struct overloaded : Ts... {
-  using Ts::operator()...;
-};
-// explicit deduction guide (not needed as of C++20)
-template <class... Ts>
-overloaded(Ts...) -> overloaded<Ts...>;
-
-void mapParametersToProto(
-  const std::map<std::string, proteus::Parameter>& parameters,
-  google::protobuf::Map<std::string, inference::InferParameter>*
-    grpc_parameters) {
-  for (const auto& [key, value] : parameters) {
-    inference::InferParameter param;
-    std::visit(
-      overloaded{[&](bool arg) { param.set_bool_param(arg); },
-                 [&](double arg) { param.set_double_param(arg); },
-                 [&](int32_t arg) { param.set_int64_param(arg); },
-                 [&](const std::string& arg) { param.set_string_param(arg); }},
-      value);
-    grpc_parameters->insert({key, param});
-  }
-}
-
 void GrpcClient::modelLoad(const std::string& model,
                            RequestParameters* parameters) {
   inference::ModelLoadRequest request;
@@ -298,258 +273,6 @@ void GrpcClient::workerUnload(const std::string& worker) {
   }
 }
 
-void mapRequestToProto(const InferenceRequest& request,
-                       inference::ModelInferRequest& grpc_request) {
-  grpc_request.set_id(request.getID());
-
-  auto* parameters = request.getParameters();
-  if (parameters != nullptr) {
-    auto params = parameters->data();
-    auto* grpc_parameters = grpc_request.mutable_parameters();
-    mapParametersToProto(params, grpc_parameters);
-  }
-
-  auto inputs = request.getInputs();
-  for (const auto& input : inputs) {
-    auto* tensor = grpc_request.add_inputs();
-
-    tensor->set_name(input.getName());
-    auto shape = input.getShape();
-    auto size = 1U;
-    for (const auto& index : shape) {
-      tensor->add_shape(index);
-      size *= index;
-    }
-    auto datatype = input.getDatatype();
-    tensor->set_datatype(datatype.str());
-    mapParametersToProto(input.getParameters()->data(),
-                         tensor->mutable_parameters());
-
-    switch (input.getDatatype()) {
-      case DataType::BOOL: {
-        auto* data = static_cast<bool*>(input.getData());
-        auto* contents = tensor->mutable_contents()->mutable_bool_contents();
-        for (size_t i = 0; i < input.getSize(); i++) {
-          contents->Add(data[i]);
-        }
-        break;
-      }
-      case DataType::UINT8: {
-        auto* data = static_cast<uint8_t*>(input.getData());
-        auto* contents = tensor->mutable_contents()->mutable_uint_contents();
-        for (size_t i = 0; i < input.getSize(); i++) {
-          contents->Add(data[i]);
-        }
-        break;
-      }
-      case DataType::UINT16: {
-        auto* data = static_cast<uint16_t*>(input.getData());
-        auto* contents = tensor->mutable_contents()->mutable_uint_contents();
-        for (size_t i = 0; i < input.getSize(); i++) {
-          contents->Add(data[i]);
-        }
-        break;
-      }
-      case DataType::UINT32: {
-        auto* data = static_cast<uint32_t*>(input.getData());
-        auto* contents = tensor->mutable_contents()->mutable_uint_contents();
-        for (size_t i = 0; i < input.getSize(); i++) {
-          contents->Add(data[i]);
-        }
-        break;
-      }
-      case DataType::UINT64: {
-        auto* data = static_cast<uint64_t*>(input.getData());
-        auto* contents = tensor->mutable_contents()->mutable_uint64_contents();
-        for (size_t i = 0; i < input.getSize(); i++) {
-          contents->Add(data[i]);
-        }
-        break;
-      }
-      case DataType::INT8: {
-        auto* data = static_cast<int8_t*>(input.getData());
-        auto* contents = tensor->mutable_contents()->mutable_int_contents();
-        for (size_t i = 0; i < input.getSize(); i++) {
-          contents->Add(data[i]);
-        }
-        break;
-      }
-      case DataType::INT16: {
-        auto* data = static_cast<int16_t*>(input.getData());
-        auto* contents = tensor->mutable_contents()->mutable_int_contents();
-        for (size_t i = 0; i < input.getSize(); i++) {
-          contents->Add(data[i]);
-        }
-        break;
-      }
-      case DataType::INT32: {
-        auto* data = static_cast<int32_t*>(input.getData());
-        auto* contents = tensor->mutable_contents()->mutable_int_contents();
-        for (size_t i = 0; i < input.getSize(); i++) {
-          contents->Add(data[i]);
-        }
-        break;
-      }
-      case DataType::INT64: {
-        auto* data = static_cast<int64_t*>(input.getData());
-        auto* contents = tensor->mutable_contents()->mutable_int64_contents();
-        for (size_t i = 0; i < input.getSize(); i++) {
-          contents->Add(data[i]);
-        }
-        break;
-      }
-      case DataType::FP16: {
-        // FIXME(varunsh): this is not handled
-        std::cout << "Writing FP16 not supported\n";
-        break;
-      }
-      case DataType::FP32: {
-        auto* data = static_cast<float*>(input.getData());
-        auto* contents = tensor->mutable_contents()->mutable_fp32_contents();
-        for (size_t i = 0; i < input.getSize(); i++) {
-          contents->Add(data[i]);
-        }
-        break;
-      }
-      case DataType::FP64: {
-        auto* data = static_cast<double*>(input.getData());
-        auto* contents = tensor->mutable_contents()->mutable_fp64_contents();
-        for (size_t i = 0; i < input.getSize(); i++) {
-          contents->Add(data[i]);
-        }
-        break;
-      }
-      case DataType::STRING: {
-        auto* data = static_cast<std::string*>(input.getData());
-        auto* contents = tensor->mutable_contents()->mutable_bytes_contents();
-        contents->Add(std::move(*data));
-        // for(size_t i = 0; i < output.getSize(); i++){
-        //   contents->Add(data->data()[i]);
-        // }
-        break;
-      }
-      default:
-        // TODO(varunsh): what should we do here?
-        std::cout << "Unknown datatype\n";
-        break;
-    }
-  }
-
-  // TODO(varunsh): skipping outputs for now
-}
-
-void mapPrototoResponse(const inference::ModelInferResponse& reply,
-                        InferenceResponse& response) {
-  response.setModel(reply.model_name());
-  response.setID(reply.id());
-
-  for (const auto& tensor : reply.outputs()) {
-    InferenceResponseOutput output;
-    output.setName(tensor.name());
-    output.setDatatype(DataType(tensor.datatype().c_str()));
-    std::vector<uint64_t> shape;
-    shape.reserve(tensor.shape_size());
-    auto size = 1U;
-    for (const auto& index : tensor.shape()) {
-      shape.push_back(static_cast<size_t>(index));
-      size *= index;
-    }
-    output.setShape(shape);
-    // TODO(varunsh): skipping parameters for now
-
-    switch (output.getDatatype()) {
-      case DataType::BOOL: {
-        auto data = std::make_shared<std::vector<char>>();
-        data->resize(size);
-        std::memcpy(data->data(), tensor.contents().bool_contents().data(),
-                    size * sizeof(char));
-        output.setData(std::reinterpret_pointer_cast<std::byte>(data));
-        break;
-      }
-      case DataType::UINT8:
-      case DataType::UINT16:
-      case DataType::UINT32: {
-        auto data = std::make_shared<std::vector<uint32_t>>();
-        data->resize(size);
-        std::memcpy(data->data(), tensor.contents().uint_contents().data(),
-                    size * sizeof(uint32_t));
-        output.setData(std::reinterpret_pointer_cast<std::byte>(data));
-        output.setDatatype(DataType::UINT32);
-        break;
-      }
-      case DataType::UINT64: {
-        auto data = std::make_shared<std::vector<uint64_t>>();
-        data->resize(size);
-        std::memcpy(data->data(), tensor.contents().uint64_contents().data(),
-                    size * sizeof(uint64_t));
-        output.setData(std::reinterpret_pointer_cast<std::byte>(data));
-        break;
-      }
-      case DataType::INT8:
-      case DataType::INT16:
-      case DataType::INT32: {
-        auto data = std::make_shared<std::vector<int32_t>>();
-        data->resize(size);
-        std::memcpy(data->data(), tensor.contents().int_contents().data(),
-                    size * sizeof(int32_t));
-        output.setData(std::reinterpret_pointer_cast<std::byte>(data));
-        output.setDatatype(DataType::INT32);
-        break;
-      }
-      case DataType::INT64: {
-        auto data = std::make_shared<std::vector<int64_t>>();
-        data->resize(size);
-        std::memcpy(data->data(), tensor.contents().int64_contents().data(),
-                    size * sizeof(int64_t));
-        output.setData(std::reinterpret_pointer_cast<std::byte>(data));
-        break;
-      }
-      case DataType::FP16: {
-        // FIXME(varunsh): this is not handled
-        std::cout << "Writing FP16 not supported\n";
-        break;
-      }
-      case DataType::FP32: {
-        auto data = std::make_shared<std::vector<float>>();
-        data->resize(size);
-        std::memcpy(data->data(), tensor.contents().fp32_contents().data(),
-                    size * sizeof(float));
-        output.setData(std::reinterpret_pointer_cast<std::byte>(data));
-        break;
-      }
-      case DataType::FP64: {
-        auto data = std::make_shared<std::vector<double>>();
-        data->resize(size);
-        std::memcpy(data->data(), tensor.contents().fp64_contents().data(),
-                    size * sizeof(double));
-        output.setData(std::reinterpret_pointer_cast<std::byte>(data));
-        break;
-      }
-      case DataType::STRING: {
-        auto data = std::make_shared<std::vector<std::byte>>();
-        data->resize(size);
-        std::memcpy(data->data(), tensor.contents().bytes_contents().data(),
-                    size * sizeof(std::byte));
-        output.setData(std::reinterpret_pointer_cast<std::byte>(data));
-        break;
-      }
-      default:
-        // TODO(varunsh): what should we do here?
-        std::cout << "Unknown datatype\n";
-        break;
-    }
-
-    // auto data = std::make_shared<std::byte[]>(size);
-    // auto data = std::shared_ptr<std::byte>(new std::byte[size],
-    //                                        std::default_delete<std::byte[]>());
-    // std::memcpy(data.get(), tensor.contents().bool_contents().data(), size);
-    // auto data = std::make_shared<std::vector<uint64_t>>();
-    // data->push_back(tensor.contents().int64_contents(0));
-    // output.setData(std::reinterpret_pointer_cast<std::byte>(data));
-    response.addOutput(output);
-  }
-}
-
 InferenceResponse runInference(inference::GRPCInferenceService::Stub* stub,
                                const std::string& model,
                                const InferenceRequest& request) {
@@ -568,7 +291,7 @@ InferenceResponse runInference(inference::GRPCInferenceService::Stub* stub,
   }
 
   InferenceResponse response;
-  mapPrototoResponse(reply, response);
+  mapProtoToResponse(reply, response);
   return response;
 }
 
diff --git a/src/proteus/clients/grpc_internal.cpp b/src/proteus/clients/grpc_internal.cpp
index bf01ac41a..18c897870 100644
--- a/src/proteus/clients/grpc_internal.cpp
+++ b/src/proteus/clients/grpc_internal.cpp
@@ -30,7 +30,8 @@
 #include "predict_api.pb.h"                       // for ModelInferResponse_...
 #include "proteus/core/data_types.hpp"            // for DataType, mapTypeToStr
 #include "proteus/core/predict_api_internal.hpp"  // for RequestParameters
-#include "proteus/helpers/declarations.hpp"       // for InferenceResponseOu...
+#include "proteus/declarations.hpp"               // for InferenceResponseOu...
+#include "proteus/util/traits.hpp"                // for is_any
 
 namespace proteus {
 
@@ -80,131 +81,153 @@ void mapProtoToParameters(
   mapProtoToParameters(params, &parameters);
 }
 
-void mapResponsetoProto(InferenceResponse response,
+// refer to cppreference for std::visit
+// helper type for the visitor #4
+template <class... Ts>
+struct overloaded : Ts... {
+  using Ts::operator()...;
+};
+// explicit deduction guide (not needed as of C++20)
+template <class... Ts>
+overloaded(Ts...) -> overloaded<Ts...>;
+
+void mapParametersToProto(
+  const std::map<std::string, proteus::Parameter>& parameters,
+  google::protobuf::Map<std::string, inference::InferParameter>*
+    grpc_parameters) {
+  for (const auto& [key, value] : parameters) {
+    inference::InferParameter param;
+    std::visit(
+      overloaded{[&](bool arg) { param.set_bool_param(arg); },
+                 [&](double arg) { param.set_double_param(arg); },
+                 [&](int32_t arg) { param.set_int64_param(arg); },
+                 [&](const std::string& arg) { param.set_string_param(arg); }},
+      value);
+    grpc_parameters->insert({key, param});
+  }
+}
+
+struct AddDataToTensor {
+  template <typename T, typename Tensor>
+  void operator()(const void* source_data, size_t size, Tensor* tensor) const {
+    const auto* data = static_cast<const T*>(source_data);
+    auto* contents = getTensorContents<T>(tensor);
+    if constexpr (std::is_same_v<T, char>) {
+      contents->Add(data);
+    } else {
+      for (auto i = 0U; i < size; ++i) {
+        contents->Add(data[i]);
+      }
+    }
+  }
+};
+
+void mapRequestToProto(const InferenceRequest& request,
+                       inference::ModelInferRequest& grpc_request) {
+  grpc_request.set_id(request.getID());
+
+  if (const auto* parameters = request.getParameters(); parameters != nullptr) {
+    auto params = parameters->data();
+    auto* grpc_parameters = grpc_request.mutable_parameters();
+    mapParametersToProto(params, grpc_parameters);
+  }
+
+  const auto& inputs = request.getInputs();
+  for (const auto& input : inputs) {
+    auto* tensor = grpc_request.add_inputs();
+
+    tensor->set_name(input.getName());
+    const auto& shape = input.getShape();
+    auto size = 1U;
+    for (const auto& index : shape) {
+      tensor->add_shape(index);
+      size *= index;
+    }
+    auto datatype = input.getDatatype();
+    tensor->set_datatype(datatype.str());
+    mapParametersToProto(input.getParameters()->data(),
+                         tensor->mutable_parameters());
+
+    switchOverTypes(AddDataToTensor(), input.getDatatype(), input.getData(),
+                    input.getSize(), tensor);
+  }
+
+  // TODO(varunsh): skipping outputs for now
+}
+
+struct SetOutputData {
+  template <typename T, typename Tensor>
+  void operator()(InferenceResponseOutput* output, size_t size,
+                  Tensor* tensor) const {
+    std::vector<std::byte> data;
+    const auto* contents = getTensorContents<T>(tensor);
+    if constexpr (std::is_same_v<T, char>) {
+      data.resize(size * sizeof(std::byte));
+      std::memcpy(data.data(), contents, size * sizeof(std::byte));
+      output->setData(std::move(data));
+    } else {
+      size_t bytes_to_copy;
+      if constexpr (std::is_same_v<T, bool>) {
+        bytes_to_copy = size * sizeof(bool);
+      } else if constexpr (util::is_any_v<T, uint8_t, uint16_t, uint32_t>) {
+        bytes_to_copy = size * sizeof(uint32_t);
+        output->setDatatype(DataType::UINT32);
+      } else if constexpr (util::is_any_v<T, int8_t, int16_t, int32_t>) {
+        bytes_to_copy = size * sizeof(int32_t);
+        output->setDatatype(DataType::INT32);
+      } else {
+        bytes_to_copy = size * sizeof(T);
+      }
+      data.resize(bytes_to_copy);
+      std::memcpy(data.data(), contents, bytes_to_copy);
+      output->setData(std::move(data));
+    }
+  }
+};
+
+void mapProtoToResponse(const inference::ModelInferResponse& reply,
+                        InferenceResponse& response) {
+  response.setModel(reply.model_name());
+  response.setID(reply.id());
+
+  for (const auto& tensor : reply.outputs()) {
+    InferenceResponseOutput output;
+    output.setName(tensor.name());
+    output.setDatatype(DataType(tensor.datatype().c_str()));
+    std::vector<uint64_t> shape;
+    shape.reserve(tensor.shape_size());
+    auto size = 1U;
+    for (const auto& index : tensor.shape()) {
+      shape.push_back(static_cast<size_t>(index));
+      size *= index;
+    }
+    output.setShape(shape);
+    // TODO(varunsh): skipping parameters for now
+    switchOverTypes(SetOutputData(), output.getDatatype(), &output, size,
+                    &tensor);
+    response.addOutput(output);
+  }
+}
+
+void mapResponseToProto(InferenceResponse response,
                         inference::ModelInferResponse& reply) {
   reply.set_model_name(response.getModel());
   reply.set_id(response.getID());
   auto outputs = response.getOutputs();
-  for (InferenceResponseOutput& output : outputs) {
+  for (const InferenceResponseOutput& output : outputs) {
     auto* tensor = reply.add_outputs();
     tensor->set_name(output.getName());
     // auto* parameters = tensor->mutable_parameters();
     tensor->set_datatype(output.getDatatype().str());
-    auto shape = output.getShape();
+    const auto& shape = output.getShape();
     auto size = 1U;
     for (const size_t& index : shape) {
       tensor->add_shape(index);
       size *= index;
     }
 
-    switch (output.getDatatype()) {
-      case DataType::BOOL: {
-        auto* data = static_cast<std::vector<bool>*>(output.getData());
-        auto* contents = tensor->mutable_contents()->mutable_bool_contents();
-        for (size_t i = 0; i < output.getSize(); i++) {
-          contents->Add(static_cast<bool>((*data)[i]));
-        }
-        break;
-      }
-      case DataType::UINT8: {
-        auto* data = static_cast<std::vector<uint8_t>*>(output.getData());
-        auto* contents = tensor->mutable_contents()->mutable_uint_contents();
-        for (size_t i = 0; i < output.getSize(); i++) {
-          contents->Add((*data)[i]);
-        }
-        break;
-      }
-      case DataType::UINT16: {
-        auto* data = static_cast<std::vector<uint16_t>*>(output.getData());
-        auto* contents = tensor->mutable_contents()->mutable_uint_contents();
-        for (size_t i = 0; i < output.getSize(); i++) {
-          contents->Add((*data)[i]);
-        }
-        break;
-      }
-      case DataType::UINT32: {
-        auto* data = static_cast<std::vector<uint32_t>*>(output.getData());
-        auto* contents = tensor->mutable_contents()->mutable_uint_contents();
-        for (size_t i = 0; i < output.getSize(); i++) {
-          contents->Add((*data)[i]);
-        }
-        break;
-      }
-      case DataType::UINT64: {
-        auto* data = static_cast<std::vector<uint64_t>*>(output.getData());
-        auto* contents = tensor->mutable_contents()->mutable_uint64_contents();
-        for (size_t i = 0; i < output.getSize(); i++) {
-          contents->Add((*data)[i]);
-        }
-        break;
-      }
-      case DataType::INT8: {
-        auto* data = static_cast<std::vector<int8_t>*>(output.getData());
-        auto* contents = tensor->mutable_contents()->mutable_int_contents();
-        for (size_t i = 0; i < output.getSize(); i++) {
-          contents->Add((*data)[i]);
-        }
-        break;
-      }
-      case DataType::INT16: {
-        auto* data = static_cast<std::vector<int16_t>*>(output.getData());
-        auto* contents = tensor->mutable_contents()->mutable_int_contents();
-        for (size_t i = 0; i < output.getSize(); i++) {
-          contents->Add((*data)[i]);
-        }
-        break;
-      }
-      case DataType::INT32: {
-        auto* data = static_cast<std::vector<int32_t>*>(output.getData());
-        auto* contents = tensor->mutable_contents()->mutable_int_contents();
-        for (size_t i = 0; i < output.getSize(); i++) {
-          contents->Add((*data)[i]);
-        }
-        break;
-      }
-      case DataType::INT64: {
-        auto* data = static_cast<std::vector<int64_t>*>(output.getData());
-        auto* contents = tensor->mutable_contents()->mutable_int64_contents();
-        for (size_t i = 0; i < output.getSize(); i++) {
-          contents->Add((*data)[i]);
-        }
-        break;
-      }
-      case DataType::FP16: {
-        // FIXME(varunsh): this is not handled
-        std::cout << "Writing FP16 not supported\n";
-        break;
-      }
-      case DataType::FP32: {
-        auto* data = static_cast<std::vector<float>*>(output.getData());
-        auto* contents = tensor->mutable_contents()->mutable_fp32_contents();
-        for (size_t i = 0; i < output.getSize(); i++) {
-          contents->Add((*data)[i]);
-        }
-        break;
-      }
-      case DataType::FP64: {
-        auto* data = static_cast<std::vector<double>*>(output.getData());
-        auto* contents = tensor->mutable_contents()->mutable_fp64_contents();
-        for (size_t i = 0; i < output.getSize(); i++) {
-          contents->Add((*data)[i]);
-        }
-        break;
-      }
-      case DataType::STRING: {
-        auto* data = static_cast<std::string*>(output.getData());
-        auto* contents = tensor->mutable_contents()->mutable_bytes_contents();
-        contents->Add(data->c_str());
-        // for(size_t i = 0; i < output.getSize(); i++){
-        //   contents->Add(data->data()[i]);
-        // }
-        break;
-      }
-      default:
-        // TODO(varunsh): what should we do here?
-        std::cout << "Unknown datatype\n";
-        break;
-    }
+    switchOverTypes(AddDataToTensor(), output.getDatatype(), output.getData(),
+                    output.getSize(), tensor);
   }
 }
 
diff --git a/src/proteus/clients/grpc_internal.hpp b/src/proteus/clients/grpc_internal.hpp
index 38d8b8202..e849c6125 100644
--- a/src/proteus/clients/grpc_internal.hpp
+++ b/src/proteus/clients/grpc_internal.hpp
@@ -20,9 +20,12 @@
 #ifndef GUARD_PROTEUS_CLIENTS_GRPC_INTERNAL
 #define GUARD_PROTEUS_CLIENTS_GRPC_INTERNAL
 
+#include <map>
 #include <string>
 
+#include "proteus/core/interface.hpp"  // for Interface
 #include "proteus/core/predict_api_internal.hpp"
+#include "proteus/util/traits.hpp"  // for is_any
 
 namespace google::protobuf {
 template <typename T, typename U>
@@ -33,21 +36,85 @@ namespace inference {
 class InferParameter;
 class ModelInferResponse;
 class ModelMetadataResponse;
+class ModelInferRequest;
 }  // namespace inference
 
 namespace proteus {
 
+void mapParametersToProto(
+  const std::map<std::string, proteus::Parameter>& parameters,
+  google::protobuf::Map<std::string, inference::InferParameter>*
+    grpc_parameters);
 RequestParametersPtr mapProtoToParameters(
   const google::protobuf::Map<std::string, inference::InferParameter>& params);
 void mapProtoToParameters(
   const google::protobuf::Map<std::string, inference::InferParameter>& params,
   RequestParameters& parameters);
-void mapResponsetoProto(InferenceResponse response,
+void mapRequestToProto(const InferenceRequest& request,
+                       inference::ModelInferRequest& grpc_request);
+void mapResponseToProto(InferenceResponse response,
                         inference::ModelInferResponse& reply);
+void mapProtoToResponse(const inference::ModelInferResponse& reply,
+                        InferenceResponse& response);
 
 void mapModelMetadataToProto(const ModelMetadata& metadata,
                              inference::ModelMetadataResponse& resp);
 
+template <typename T, typename Tensor>
+constexpr auto* getTensorContents(Tensor* tensor) {
+  if constexpr (std::is_same_v<T, bool>) {
+    if constexpr (std::is_const_v<Tensor>) {
+      return tensor->contents().bool_contents().data();
+    } else {
+      return tensor->mutable_contents()->mutable_bool_contents();
+    }
+  } else if constexpr (util::is_any_v<T, uint8_t, uint16_t, uint32_t>) {
+    if constexpr (std::is_const_v<Tensor>) {
+      return tensor->contents().uint_contents().data();
+    } else {
+      return tensor->mutable_contents()->mutable_uint_contents();
+    }
+  } else if constexpr (std::is_same_v<T, uint64_t>) {
+    if constexpr (std::is_const_v<Tensor>) {
+      return tensor->contents().uint64_contents().data();
+    } else {
+      return tensor->mutable_contents()->mutable_uint64_contents();
+    }
+  } else if constexpr (util::is_any_v<T, int8_t, int16_t, int32_t>) {
+    if constexpr (std::is_const_v<Tensor>) {
+      return tensor->contents().int_contents().data();
+    } else {
+      return tensor->mutable_contents()->mutable_int_contents();
+    }
+  } else if constexpr (std::is_same_v<T, int64_t>) {
+    if constexpr (std::is_const_v<Tensor>) {
+      return tensor->contents().int64_contents().data();
+    } else {
+      return tensor->mutable_contents()->mutable_int64_contents();
+    }
+  } else if constexpr (util::is_any_v<T, float>) {
+    if constexpr (std::is_const_v<Tensor>) {
+      return tensor->contents().fp32_contents().data();
+    } else {
+      return tensor->mutable_contents()->mutable_fp32_contents();
+    }
+  } else if constexpr (std::is_same_v<T, double>) {
+    if constexpr (std::is_const_v<Tensor>) {
+      return tensor->contents().fp64_contents().data();
+    } else {
+      return tensor->mutable_contents()->mutable_fp64_contents();
+    }
+  } else if constexpr (std::is_same_v<T, char>) {
+    if constexpr (std::is_const_v<Tensor>) {
+      return tensor->contents().bytes_contents().data();
+    } else {
+      return tensor->mutable_contents()->mutable_bytes_contents();
+    }
+  } else {
+    static_assert(!sizeof(T), "Invalid type to AddDataToTensor");
+  }
+}
+
 }  // namespace proteus
 
 #endif  // GUARD_PROTEUS_CLIENTS_GRPC_INTERNAL
diff --git a/src/proteus/clients/http.cpp b/src/proteus/clients/http.cpp
index e99f9ef7a..15a2fe939 100644
--- a/src/proteus/clients/http.cpp
+++ b/src/proteus/clients/http.cpp
@@ -92,13 +92,28 @@ void check_error(drogon::ReqResult result) {
   }
 }
 
-ServerMetadata HttpClient::serverMetadata() {
-  auto* client = this->impl_->getClient();
-
+drogon::HttpRequestPtr createGetRequest(const std::string& path,
+                                        const StringMap& headers) {
   auto req = drogon::HttpRequest::newHttpRequest();
   req->setMethod(drogon::Get);
-  req->setPath("/v2");
-  addHeaders(req, headers_);
+  req->setPath(path);
+  addHeaders(req, headers);
+  return req;
+}
+
+drogon::HttpRequestPtr createPostRequest(const Json::Value& json,
+                                         const std::string& path,
+                                         const StringMap& headers) {
+  auto req = drogon::HttpRequest::newHttpJsonRequest(json);
+  req->setMethod(drogon::Post);
+  req->setPath(path);
+  addHeaders(req, headers);
+  return req;
+}
+
+ServerMetadata HttpClient::serverMetadata() {
+  auto* client = this->impl_->getClient();
+  auto req = createGetRequest("/v2", headers_);
 
   auto [result, response] = client->sendRequest(req);
   check_error(result);
@@ -118,12 +133,7 @@ ServerMetadata HttpClient::serverMetadata() {
 
 bool HttpClient::serverLive() {
   auto* client = this->impl_->getClient();
-
-  auto req = drogon::HttpRequest::newHttpRequest();
-  req->setMethod(drogon::Get);
-  auto path = "/v2/health/live";
-  req->setPath(path);
-  addHeaders(req, headers_);
+  auto req = createGetRequest("/v2/health/live", headers_);
 
   auto [result, response] = client->sendRequest(req);
   if (result != drogon::ReqResult::Ok) {
@@ -134,12 +144,7 @@ bool HttpClient::serverLive() {
 
 bool HttpClient::serverReady() {
   auto* client = this->impl_->getClient();
-
-  auto req = drogon::HttpRequest::newHttpRequest();
-  req->setMethod(drogon::Get);
-  auto path = "/v2/health/ready";
-  req->setPath(path);
-  addHeaders(req, headers_);
+  auto req = createGetRequest("/v2/health/ready", headers_);
 
   auto [result, response] = client->sendRequest(req);
   check_error(result);
@@ -148,12 +153,7 @@ bool HttpClient::serverReady() {
 
 bool HttpClient::modelReady(const std::string& model) {
   auto* client = this->impl_->getClient();
-
-  auto req = drogon::HttpRequest::newHttpRequest();
-  req->setMethod(drogon::Get);
-  auto path = "/v2/models/" + model + "/ready";
-  req->setPath(path);
-  addHeaders(req, headers_);
+  auto req = createGetRequest("/v2/models/" + model + "/ready", headers_);
 
   auto [result, response] = client->sendRequest(req);
   check_error(result);
@@ -162,11 +162,7 @@ bool HttpClient::modelReady(const std::string& model) {
 
 ModelMetadata HttpClient::modelMetadata(const std::string& model) {
   auto* client = this->impl_->getClient();
-  auto req = drogon::HttpRequest::newHttpRequest();
-  req->setMethod(drogon::Get);
-  auto path = "/v2/models/" + model;
-  req->setPath(path);
-  addHeaders(req, headers_);
+  auto req = createGetRequest("/v2/models/" + model, headers_);
 
   auto [result, response] = client->sendRequest(req);
   check_error(result);
@@ -183,11 +179,8 @@ void HttpClient::modelLoad(const std::string& model,
     json = mapParametersToJson(parameters);
   }
 
-  auto req = drogon::HttpRequest::newHttpJsonRequest(json);
-  req->setMethod(drogon::Post);
-  auto path = "/v2/repository/models/" + model + "/load";
-  req->setPath(path);
-  addHeaders(req, headers_);
+  auto req = createPostRequest(json, "/v2/repository/models/" + model + "/load",
+                               headers_);
 
   auto [result, response] = client->sendRequest(req);
   check_error(result);
@@ -200,11 +193,8 @@ void HttpClient::modelUnload(const std::string& model) {
   auto* client = this->impl_->getClient();
 
   Json::Value json;
-  auto req = drogon::HttpRequest::newHttpJsonRequest(json);
-  req->setMethod(drogon::Post);
-  auto path = "/v2/repository/models/" + model + "/unload";
-  req->setPath(path);
-  addHeaders(req, headers_);
+  auto req = createPostRequest(
+    json, "/v2/repository/models/" + model + "/unload", headers_);
 
   auto [result, response] = client->sendRequest(req);
   check_error(result);
@@ -223,11 +213,8 @@ std::string HttpClient::workerLoad(const std::string& model,
     json = mapParametersToJson(parameters);
   }
 
-  auto req = drogon::HttpRequest::newHttpJsonRequest(json);
-  req->setMethod(drogon::Post);
-  auto path = "/v2/workers/" + model + "/load";
-  req->setPath(path);
-  addHeaders(req, headers_);
+  auto req =
+    createPostRequest(json, "/v2/workers/" + model + "/load", headers_);
 
   auto [result, response] = client->sendRequest(req);
   check_error(result);
@@ -241,11 +228,8 @@ void HttpClient::workerUnload(const std::string& model) {
   auto* client = this->impl_->getClient();
 
   Json::Value json;
-  auto req = drogon::HttpRequest::newHttpJsonRequest(json);
-  req->setMethod(drogon::Post);
-  auto path = "/v2/workers/" + model + "/unload";
-  req->setPath(path);
-  addHeaders(req, headers_);
+  auto req =
+    createPostRequest(json, "/v2/workers/" + model + "/unload", headers_);
 
   auto [result, response] = client->sendRequest(req);
   check_error(result);
@@ -262,11 +246,7 @@ InferenceResponse runInference(drogon::HttpClient* client,
   assert(!request.getInputs().empty());
 
   auto json = mapRequestToJson(request);
-  auto req = drogon::HttpRequest::newHttpJsonRequest(json);
-  req->setMethod(drogon::Post);
-  auto path = "/v2/models/" + model + "/infer";
-  req->setPath(path);
-  addHeaders(req, headers);
+  auto req = createPostRequest(json, "/v2/models/" + model + "/infer", headers);
 
   auto [result, response] = client->sendRequest(req);
   check_error(result);
@@ -291,12 +271,7 @@ InferenceResponse HttpClient::modelInfer(const std::string& model,
 
 std::vector<std::string> HttpClient::modelList() {
   auto* client = this->impl_->getClient();
-
-  auto req = drogon::HttpRequest::newHttpRequest();
-  req->setMethod(drogon::Get);
-  const std::string path = "/v2/models";
-  req->setPath(path);
-  addHeaders(req, headers_);
+  auto req = createGetRequest("/v2/models", headers_);
 
   auto [result, response] = client->sendRequest(req);
   check_error(result);
@@ -320,11 +295,7 @@ bool HttpClient::hasHardware(const std::string& name, int num) {
   Json::Value json;
   json["name"] = name;
   json["num"] = num;
-  auto req = drogon::HttpRequest::newHttpJsonRequest(json);
-  req->setMethod(drogon::Get);
-  const std::string path = "/v2/hardware";
-  req->setPath(path);
-  addHeaders(req, headers_);
+  auto req = createPostRequest(json, "/v2/hardware", headers_);
 
   auto [result, response] = client->sendRequest(req);
   check_error(result);
diff --git a/src/proteus/clients/http_internal.cpp b/src/proteus/clients/http_internal.cpp
index 17fbbfd3d..1580f8e5a 100644
--- a/src/proteus/clients/http_internal.cpp
+++ b/src/proteus/clients/http_internal.cpp
@@ -40,8 +40,9 @@
 #include "proteus/core/exceptions.hpp"            // invalid_argument
 #include "proteus/core/interface.hpp"             // for InterfaceType, Inte...
 #include "proteus/core/predict_api_internal.hpp"  // for InferenceRequestOutput
-#include "proteus/helpers/compression.hpp"        // for z_decompress
 #include "proteus/observation/logging.hpp"        // for Logger
+#include "proteus/util/compression.hpp"           // for z_decompress
+#include "proteus/util/traits.hpp"                // for is_any
 
 namespace proteus {
 
@@ -92,18 +93,54 @@ Json::Value mapParametersToJson(RequestParameters *parameters) {
   return json;
 }
 
-template <typename T, typename Fn>
-void setOutputData(const Json::Value &json, InferenceResponseOutput *output,
-                   Fn f) {
-  auto data = std::make_shared<std::vector<T>>();
-  data->reserve(json.size());
-  for (const auto &datum : json) {
-    data->push_back((datum.*f)());
+template <typename T>
+constexpr auto jsonValueToType(const Json::Value &datum) {
+  if constexpr (std::is_same_v<T, bool>) {
+    return datum.asBool();
+  } else if constexpr (util::is_any_v<T, uint8_t, uint16_t, uint32_t>) {
+    return datum.asUInt();
+  } else if constexpr (std::is_same_v<T, uint64_t>) {
+    return datum.asUInt64();
+  } else if constexpr (util::is_any_v<T, int8_t, int16_t, int32_t>) {
+    return datum.asInt();
+  } else if constexpr (std::is_same_v<T, int64_t>) {
+    return datum.asInt64();
+  } else if constexpr (util::is_any_v<T, float>) {
+    return datum.asFloat();
+  } else if constexpr (std::is_same_v<T, double>) {
+    return datum.asDouble();
+  } else if constexpr (std::is_same_v<T, char>) {
+    return datum.asString();
+  } else {
+    static_assert(!sizeof(T), "Invalid type to SetOutputData");
   }
-  auto data_cast = std::reinterpret_pointer_cast<std::byte>(data);
-  output->setData(std::move(data_cast));
 }
 
+struct SetOutputData {
+  template <typename T>
+  void operator()(const Json::Value &json,
+                  InferenceResponseOutput *output) const {
+    if constexpr (std::is_same_v<T, char>) {
+      assert(json.size() == 1);
+      auto str = json[0].asString();
+      std::vector<std::byte> data;
+      data.resize(str.length());
+      memcpy(data.data(), str.data(), str.length());
+      output->setData(std::move(data));
+    } else {
+      std::vector<std::byte> data;
+      data.resize(json.size() * sizeof(T));
+      auto *ptr = data.data();
+      for (const auto &datum : json) {
+        auto raw_datum = static_cast<T>(jsonValueToType<T>(datum));
+        memcpy(ptr, &raw_datum, sizeof(T));
+        ptr += sizeof(T);
+      }
+      output->setData(std::move(data));
+    }
+  }
+};
+
 InferenceResponse mapJsonToResponse(Json::Value *json) {
   InferenceResponse response;
   response.setModel(json->get("model_name", "").asString());
@@ -124,83 +161,41 @@ InferenceResponse mapJsonToResponse(Json::Value *json) {
     }
     output.setShape(shape);
     const auto &json_data = json_output["data"];
-    switch (output.getDatatype()) {
-      case DataType::BOOL: {
-        setOutputData<bool>(json_data, &output, &Json::Value::asBool);
-        break;
-      }
-      case DataType::UINT8: {
-        setOutputData<uint8_t>(json_data, &output, &Json::Value::asUInt);
-        break;
-      }
-      case DataType::UINT16: {
-        setOutputData<uint16_t>(json_data, &output, &Json::Value::asUInt);
-        break;
-      }
-      case DataType::UINT32: {
-        setOutputData<uint32_t>(json_data, &output, &Json::Value::asUInt);
-        break;
-      }
-      case DataType::UINT64: {
-        setOutputData<uint64_t>(json_data, &output, &Json::Value::asUInt64);
-        break;
-      }
-      case DataType::INT8: {
-        setOutputData<int8_t>(json_data, &output, &Json::Value::asInt);
-        break;
-      }
-      case DataType::INT16: {
-        setOutputData<int16_t>(json_data, &output, &Json::Value::asInt);
-        break;
-      }
-      case DataType::INT32: {
-        setOutputData<int32_t>(json_data, &output, &Json::Value::asInt);
-        break;
-      }
-      case DataType::INT64: {
-        setOutputData<int64_t>(json_data, &output, &Json::Value::asInt64);
-        break;
-      }
-      case DataType::FP16: {
-        // FIXME(varunsh): this is not handled
-        throw invalid_argument("Writing FP16 not supported at this time");
-      }
-      case DataType::FP32: {
-        setOutputData<float>(json_data, &output, &Json::Value::asFloat);
-        break;
-      }
-      case DataType::FP64: {
-        setOutputData<double>(json_data, &output, &Json::Value::asDouble);
-        break;
-      }
-      case DataType::STRING: {
-        auto data = std::make_shared<std::string>();
-        assert(json_data.size() == 1);
-        auto str = json_data[0].asString();
-        data->reserve(str.size());
-        data->assign(str);
-        auto data_cast = std::reinterpret_pointer_cast<std::byte>(data);
-        output.setData(std::move(data_cast));
-        break;
-      }
-      default:
-        // TODO(varunsh): what should we do here?
-        std::cout << "Unknown datatype\n";
-        break;
-    }
+    switchOverTypes(SetOutputData(), output.getDatatype(), json_data, &output);
     response.addOutput(output);
   }
 
   return response;
 }
 
-template <typename T, typename C = T>
-void setInputData(Json::Value &json, const InferenceRequestInput *input) {
-  auto *data = static_cast<T *>(input->getData());
-  for (size_t i = 0; i < input->getSize(); i++) {
-    json.append(static_cast<C>(data[i]));
+struct SetInputData {
+  template <typename T>
+  void operator()(Json::Value *json, void *src_data, size_t src_size) const {
+    auto *data = static_cast<T *>(src_data);
+    if constexpr (std::is_same_v<T, char>) {
+      std::string str{data, src_size};
+      json->append(str);
+    } else {
+      constexpr auto getData = [](const T *data_ptr, size_t index) {
+        if constexpr (std::is_same_v<T, uint64_t>) {
+          return static_cast<Json::UInt64>(data_ptr[index]);
+        } else if constexpr (std::is_same_v<T, int64_t>) {
+          return static_cast<Json::Int64>(data_ptr[index]);
+        } else if constexpr (util::is_any_v<T, bool, uint8_t, uint16_t,
+                                            uint32_t, int8_t, int16_t, int32_t,
+                                            float, double>) {
+          return data_ptr[index];
+        } else {
+          static_assert(!sizeof(T), "Invalid type to SetInputData");
+        }
+      };
+
+      for (auto i = 0U; i < src_size; ++i) {
+        json->append(getData(data, i));
+      }
+    }
   }
-}
+};
 
 Json::Value mapRequestToJson(const InferenceRequest &request) {
   Json::Value json;
@@ -223,66 +218,8 @@ Json::Value mapRequestToJson(const InferenceRequest &request) {
       json_input["shape"].append(static_cast<Json::UInt64>(index));
     }
     json_input["data"] = Json::arrayValue;
-    switch (input.getDatatype()) {
-      case DataType::BOOL: {
-        setInputData<bool>(json_input["data"], &input);
-        break;
-      }
-      case DataType::UINT8: {
-        setInputData<uint8_t>(json_input["data"], &input);
-        break;
-      }
-      case DataType::UINT16: {
-        setInputData<uint16_t>(json_input["data"], &input);
-        break;
-      }
-      case DataType::UINT32: {
-        setInputData<uint32_t>(json_input["data"], &input);
-        break;
-      }
-      case DataType::UINT64: {
-        setInputData<uint64_t, Json::UInt64>(json_input["data"], &input);
-        break;
-      }
-      case DataType::INT8: {
-        setInputData<int8_t>(json_input["data"], &input);
-        break;
-      }
-      case DataType::INT16: {
-        setInputData<int16_t>(json_input["data"], &input);
-        break;
-      }
-      case DataType::INT32: {
-        setInputData<int32_t>(json_input["data"], &input);
-        break;
-      }
-      case DataType::INT64: {
-        setInputData<int64_t, Json::Int64>(json_input["data"], &input);
-        break;
-      }
-      case DataType::FP16: {
-        // FIXME(varunsh): this is not handled
-        std::cout << "Writing FP16 not supported\n";
-        break;
-      }
-      case DataType::FP32: {
-        setInputData<float>(json_input["data"], &input);
-        break;
-      }
-      case DataType::FP64: {
-        setInputData<double>(json_input["data"], &input);
-        break;
-      }
-      case DataType::STRING: {
-        auto *data = static_cast<std::string *>(input.getData());
-        json_input["data"].append(*data);
-        break;
-      }
-      default:
-        // TODO(varunsh): what should we do here?
-        std::cout << "Unknown datatype\n";
-        break;
-    }
+    switchOverTypes(SetInputData(), input.getDatatype(), &(json_input["data"]),
+                    input.getData(), input.getSize());
     json["inputs"].append(json_input);
   }
 
@@ -291,6 +228,18 @@ Json::Value mapRequestToJson(const InferenceRequest &request) {
   return json;
 }
 
+struct WriteData {
+  template <typename T>
+  size_t operator()(Buffer *buffer, const Json::Value &value,
+                    size_t offset) const {
+    if constexpr (std::is_same_v<T, char>) {
+      return buffer->write(jsonValueToType<T>(value), offset);
+    } else {
+      return buffer->write(static_cast<T>(jsonValueToType<T>(value)), offset);
+    }
+  }
+};
+
 template <>
 class InferenceRequestInputBuilder<std::shared_ptr<Json::Value>> {
  public:
@@ -302,7 +251,6 @@ class InferenceRequestInputBuilder<std::shared_ptr<Json::Value>> {
 #endif
     input.data_ = input_buffer->data();
 
-    input.shared_data_ = nullptr;
     if (!req->isMember("name")) {
       throw invalid_argument("No 'name' key present in request input");
     }
@@ -334,62 +282,8 @@ class InferenceRequestInputBuilder<std::shared_ptr<Json::Value>> {
     auto data = req->get("data", Json::arrayValue);
     try {
       for (auto const &i : data) {
-        switch (input.dataType_) {
-          case DataType::BOOL:
-            offset = input_buffer->write(i.asBool(), offset);
-            break;
-          case DataType::UINT8:
-            offset =
-              input_buffer->write(static_cast<uint8_t>(i.asUInt()), offset);
-            break;
-          case DataType::UINT16:
-            offset =
-              input_buffer->write(static_cast<uint16_t>(i.asUInt()), offset);
-            break;
-          case DataType::UINT32:
-            offset =
-              input_buffer->write(static_cast<uint32_t>(i.asUInt()), offset);
-            break;
-          case DataType::UINT64:
-            offset =
-              input_buffer->write(static_cast<uint64_t>(i.asUInt64()), offset);
-            break;
-          case DataType::INT8:
-            offset =
-              input_buffer->write(static_cast<int8_t>(i.asInt()), offset);
-            break;
-          case DataType::INT16:
-            offset =
-              input_buffer->write(static_cast<int16_t>(i.asInt()), offset);
-            break;
-          case DataType::INT32:
-            offset =
-              input_buffer->write(static_cast<int32_t>(i.asInt()), offset);
-            break;
-          case DataType::INT64:
-            offset =
-              input_buffer->write(static_cast<int64_t>(i.asInt64()), offset);
-            break;
-          case DataType::FP16:
-            // FIXME(varunsh): this is not handled
-            PROTEUS_LOG_WARN(logger, "Writing FP16 not supported");
-            break;
-          case DataType::FP32:
-            offset =
-              input_buffer->write(static_cast<float>(i.asFloat()), offset);
-            break;
-          case DataType::FP64:
-            offset =
-              input_buffer->write(static_cast<double>(i.asDouble()), offset);
-            break;
-          case DataType::STRING:
-            offset = input_buffer->write(i.asString(), offset);
-            break;
-          default:
-            // TODO(varunsh): what should we do here?
-            PROTEUS_LOG_WARN(logger, "Unknown datatype");
-            break;
-        }
+        offset = switchOverTypes(WriteData(), input.dataType_, input_buffer, i,
+                                 offset);
       }
     } catch (const Json::LogicError &) {
       throw invalid_argument(
@@ -545,7 +439,7 @@ std::shared_ptr<Json::Value> parseJson(const drogon::HttpRequest *req) {
   PROTEUS_LOG_DEBUG(logger, "Failed to interpret body as JSON data");
 
   // if it's still not valid, attempt to uncompress the body and convert to JSON
-  auto body_decompress = z_decompress(body.data(), body.length());
+  auto body_decompress = util::z_decompress(body.data(), body.length());
   success = reader->parse(body_decompress.data(),
                           body_decompress.data() + body_decompress.size(),
                           root.get(), &errors);
@@ -569,8 +463,9 @@ size_t DrogonHttp::getInputSize() {
   }
   auto inputs = this->json_->get("inputs", Json::arrayValue);
   if (!inputs.isArray()) {
-    throw invalid_argument("'inputs' is not an array");
+    throw invalid_argument("'inputs' is not an arrafy");
   }
+
   return inputs.size();
 }
 
@@ -580,114 +475,20 @@ Json::Value parseResponse(InferenceResponse response) {
   ret["outputs"] = Json::arrayValue;
   ret["id"] = response.getID();
   auto outputs = response.getOutputs();
-  for (InferenceResponseOutput &output : outputs) {
+  for (const InferenceResponseOutput &output : outputs) {
     Json::Value json_output;
     json_output["name"] = output.getName();
     json_output["parameters"] = Json::objectValue;
     json_output["data"] = Json::arrayValue;
     json_output["shape"] = Json::arrayValue;
     json_output["datatype"] = output.getDatatype().str();
-    auto shape = output.getShape();
+    const auto &shape = output.getShape();
     for (const size_t &index : shape) {
       json_output["shape"].append(static_cast<Json::UInt>(index));
     }
 
-    switch (output.getDatatype()) {
-      case DataType::BOOL: {
-        auto *data = static_cast<std::vector<bool> *>(output.getData());
-        for (size_t i = 0; i < output.getSize(); i++) {
-          json_output["data"].append(static_cast<bool>((*data)[i]));
-        }
-        break;
-      }
-      case DataType::UINT8: {
-        auto *data = static_cast<std::vector<uint8_t> *>(output.getData());
-        for (size_t i = 0; i < output.getSize(); i++) {
-          json_output["data"].append((*data)[i]);
-        }
-        break;
-      }
-      case DataType::UINT16: {
-        auto *data = static_cast<std::vector<uint16_t> *>(output.getData());
-        for (size_t i = 0; i < output.getSize(); i++) {
-          json_output["data"].append((*data)[i]);
-        }
-        break;
-      }
-      case DataType::UINT32: {
-        auto *data = static_cast<std::vector<uint32_t> *>(output.getData());
-        for (size_t i = 0; i < output.getSize(); i++) {
-          json_output["data"].append((*data)[i]);
-        }
-        break;
-      }
-      case DataType::UINT64: {
-        auto *data = static_cast<std::vector<uint64_t> *>(output.getData());
-        for (size_t i = 0; i < output.getSize(); i++) {
-          json_output["data"].append(static_cast<Json::UInt64>((*data)[i]));
-        }
-        break;
-      }
-      case DataType::INT8: {
-        auto *data = static_cast<std::vector<int8_t> *>(output.getData());
-        for (size_t i = 0; i < output.getSize(); i++) {
-          json_output["data"].append((*data)[i]);
-        }
-        break;
-      }
-      case DataType::INT16: {
-        auto *data = static_cast<std::vector<int16_t> *>(output.getData());
-        for (size_t i = 0; i < output.getSize(); i++) {
-          json_output["data"].append((*data)[i]);
-        }
-        break;
-      }
-      case DataType::INT32: {
-        auto *data = static_cast<std::vector<int32_t> *>(output.getData());
-        for (size_t i = 0; i < output.getSize(); i++) {
-          json_output["data"].append((*data)[i]);
-        }
-        break;
-      }
-      case DataType::INT64: {
-        auto *data = static_cast<std::vector<int64_t> *>(output.getData());
-        for (size_t i = 0; i < output.getSize(); i++) {
-          json_output["data"].append(static_cast<Json::Int64>((*data)[i]));
-        }
-        break;
-      }
-      case DataType::FP16: {
-        // FIXME(varunsh): this is not handled
-        std::cout << "Writing FP16 not supported\n";
-        break;
-      }
-      case DataType::FP32: {
-        auto *data = static_cast<std::vector<float> *>(output.getData());
-        for (size_t i = 0; i < output.getSize(); i++) {
-          json_output["data"].append((*data)[i]);
-        }
-        break;
-      }
-      case DataType::FP64: {
-        auto *data = static_cast<std::vector<double> *>(output.getData());
-        for (size_t i = 0; i < output.getSize(); i++) {
-          json_output["data"].append((*data)[i]);
-        }
-        break;
-      }
-      case DataType::STRING: {
-        auto *data = static_cast<std::string *>(output.getData());
-        json_output["data"].append(*data);
-        // for(size_t i = 0; i < output.getSize(); i++){
-        //   json_output["data"].append(data->data()[i]);
-        // }
-        break;
-      }
-      default:
-        // TODO(varunsh): what should we do here?
-        std::cout << "Unknown datatype\n";
-        break;
-    }
+    switchOverTypes(SetInputData(), output.getDatatype(),
+                    &(json_output["data"]), output.getData(), output.getSize());
     ret["outputs"].append(json_output);
   }
   return ret;
diff --git a/src/proteus/clients/http_internal.hpp b/src/proteus/clients/http_internal.hpp
index 2f5d25a08..572dcebad 100644
--- a/src/proteus/clients/http_internal.hpp
+++ b/src/proteus/clients/http_internal.hpp
@@ -34,7 +34,7 @@
 #include "proteus/build_options.hpp"              // for PROTEUS_ENABLE_TRACING
 #include "proteus/core/interface.hpp"             // for Interface
 #include "proteus/core/predict_api_internal.hpp"  // for InferenceRequestBui...
-#include "proteus/helpers/declarations.hpp"       // for BufferRawPtrs, Infe...
+#include "proteus/declarations.hpp"               // for BufferRawPtrs, Infe...
 
 namespace proteus {
 
diff --git a/src/proteus/clients/native.cpp b/src/proteus/clients/native.cpp
index d394a8c88..5e9c87f8d 100644
--- a/src/proteus/clients/native.cpp
+++ b/src/proteus/clients/native.cpp
@@ -30,9 +30,9 @@
 #include "proteus/core/api.hpp"                 // for modelLoad, workerLoad
 #include "proteus/core/exceptions.hpp"          // for invalid_argument
 #include "proteus/core/interface.hpp"           // for Interface
-#include "proteus/helpers/string.hpp"           // for toLower
 #include "proteus/observation/metrics.hpp"      // for Metrics, MetricCounte...
 #include "proteus/observation/tracing.hpp"      // for startTrace, Trace
+#include "proteus/util/string.hpp"              // for toLower
 
 namespace proteus {
 
@@ -48,7 +48,7 @@ ModelMetadata NativeClient::modelMetadata(const std::string& model) {
 
 void NativeClient::modelLoad(const std::string& model,
                              RequestParameters* parameters) {
-  auto model_lower = toLower(model);
+  auto model_lower = util::toLower(model);
   if (parameters == nullptr) {
     RequestParameters params;
     ::proteus::modelLoad(model_lower, &params);
@@ -59,7 +59,7 @@ void NativeClient::modelLoad(const std::string& model,
 
 std::string NativeClient::workerLoad(const std::string& model,
                                      RequestParameters* parameters) {
-  auto model_lower = toLower(model);
+  auto model_lower = util::toLower(model);
   if (parameters == nullptr) {
     RequestParameters params;
     return ::proteus::workerLoad(model_lower, &params);
@@ -95,12 +95,12 @@ InferenceResponse NativeClient::modelInfer(const std::string& model,
 }
 
 void NativeClient::modelUnload(const std::string& model) {
-  auto model_lower = toLower(model);
+  auto model_lower = util::toLower(model);
   ::proteus::modelUnload(model);
 }
 
 void NativeClient::workerUnload(const std::string& model) {
-  auto model_lower = toLower(model);
+  auto model_lower = util::toLower(model);
   ::proteus::workerUnload(model);
 }
 
diff --git a/src/proteus/clients/native_internal.cpp b/src/proteus/clients/native_internal.cpp
index 19830e893..608bd800b 100644
--- a/src/proteus/clients/native_internal.cpp
+++ b/src/proteus/clients/native_internal.cpp
@@ -48,7 +48,6 @@ class InferenceRequestInputBuilder<InferenceRequestInput> {
                                      Buffer *input_buffer, size_t offset) {
     InferenceRequestInput input;
     input.data_ = req.data_;
-    input.shared_data_ = nullptr;
     input.name_ = req.name_;
     input.shape_.reserve(req.shape_.size());
     input.shape_ = req.shape_;
diff --git a/src/proteus/clients/native_internal.hpp b/src/proteus/clients/native_internal.hpp
index 377077a6d..29ebd7552 100644
--- a/src/proteus/clients/native_internal.hpp
+++ b/src/proteus/clients/native_internal.hpp
@@ -26,9 +26,9 @@
 #include <memory>     // for shared_ptr
 #include <vector>     // for vector
 
-#include "proteus/core/interface.hpp"        // for Interface
-#include "proteus/core/predict_api.hpp"      // for InferenceRequest, Infere...
-#include "proteus/helpers/declarations.hpp"  // for BufferRawPtrs
+#include "proteus/core/interface.hpp"    // for Interface
+#include "proteus/core/predict_api.hpp"  // for InferenceRequest, Infere...
+#include "proteus/declarations.hpp"      // for BufferRawPtrs
 
 namespace proteus {
 
diff --git a/src/proteus/core/api.hpp b/src/proteus/core/api.hpp
index ba172b903..d43a15aea 100644
--- a/src/proteus/core/api.hpp
+++ b/src/proteus/core/api.hpp
@@ -24,7 +24,7 @@
 #include <string>
 #include <vector>
 
-#include "proteus/helpers/declarations.hpp"
+#include "proteus/declarations.hpp"
 
 namespace proteus {
 
diff --git a/src/proteus/core/interface.hpp b/src/proteus/core/interface.hpp
index f77b31124..2363b39e7 100644
--- a/src/proteus/core/interface.hpp
+++ b/src/proteus/core/interface.hpp
@@ -26,10 +26,10 @@
 #include <memory>     // for shared_ptr
 #include <vector>     // for vector
 
-#include "proteus/build_options.hpp"         // for PROTEUS_ENABLE_LOGGING
-#include "proteus/helpers/declarations.hpp"  // for BufferRawPtrs
-#include "proteus/observation/logging.hpp"   // for Logger, Loggers, Loggers...
-#include "proteus/observation/tracing.hpp"   // for TracePtr
+#include "proteus/build_options.hpp"        // for PROTEUS_ENABLE_LOGGING
+#include "proteus/declarations.hpp"         // for BufferRawPtrs
+#include "proteus/observation/logging.hpp"  // for Logger, Loggers, Loggers...
+#include "proteus/observation/tracing.hpp"  // for TracePtr
 
 namespace proteus {
 class InferenceRequest;
diff --git a/src/proteus/core/manager.cpp b/src/proteus/core/manager.cpp
index 3d3c5b503..c09ea53d5 100644
--- a/src/proteus/core/manager.cpp
+++ b/src/proteus/core/manager.cpp
@@ -26,7 +26,7 @@
 #include "proteus/build_options.hpp"     // for kMaxModelNameSize
 #include "proteus/core/exceptions.hpp"   // for invalid_argument
 #include "proteus/core/worker_info.hpp"  // for WorkerInfo
-#include "proteus/helpers/thread.hpp"    // for setThreadName
+#include "proteus/util/thread.hpp"       // for setThreadName
 #include "proteus/workers/worker.hpp"    // for Worker
 
 namespace proteus {
@@ -134,7 +134,7 @@ void Manager::shutdown() {
 
 void Manager::update_manager(UpdateCommandQueue* input_queue) {
   PROTEUS_LOG_DEBUG(logger_, "Starting the Manager update thread");
-  setThreadName("manager");
+  util::setThreadName("manager");
   std::shared_ptr<UpdateCommand> request;
   bool run = true;
   while (run) {
diff --git a/src/proteus/core/manager.hpp b/src/proteus/core/manager.hpp
index 8c2c62abd..49846c2a3 100644
--- a/src/proteus/core/manager.hpp
+++ b/src/proteus/core/manager.hpp
@@ -33,8 +33,8 @@
 #include "proteus/build_options.hpp"        // for PROTEUS_ENABLE_LOGGING
 #include "proteus/core/predict_api.hpp"     // for RequestParameters
 #include "proteus/core/worker_info.hpp"     // for WorkerInfo
-#include "proteus/helpers/queue.hpp"        // for BlockingConcurrentQueue
 #include "proteus/observation/logging.hpp"  // for LoggerPtr
+#include "proteus/util/queue.hpp"           // for BlockingConcurrentQueue
 
 // IWYU pragma: no_forward_declare proteus::RequestParameters
 // IWYU pragma: no_forward_declare proteus::WorkerInfo
diff --git a/src/proteus/core/predict_api.cpp b/src/proteus/core/predict_api.cpp
index 3f739909a..1e07f284d 100644
--- a/src/proteus/core/predict_api.cpp
+++ b/src/proteus/core/predict_api.cpp
@@ -20,6 +20,8 @@
 #include "proteus/core/predict_api.hpp"
 
 #include <algorithm>  // for copy
+#include <cassert>    // for assert
+#include <cstring>    // for memcpy
 #include <iterator>   // for back_insert_iterator, back_inse...
 #include <numeric>    // for accumulate
 #include <utility>    // for pair, make_pair, move
@@ -66,8 +68,129 @@ std::map<std::string, Parameter> RequestParameters::data() const {
   return parameters_;
 }
 
+size_t RequestParameters::serializeSize() const {
+  // 1 for num of parameters plus 3 for each parameter for type index, key size
+  // and value size
+  auto size = ((this->size() * 3) + 1) * sizeof(size_t);
+  for (const auto &[key, value] : parameters_) {
+    size += key.size();
+    std::visit(
+      [&size](const auto &param) {
+        using T = std::decay_t<decltype(param)>;
+        if constexpr (std::is_same_v<T, std::string>) {
+          size += param.size();
+        } else {
+          size += sizeof(param);
+        }
+      },
+      value);
+  }
+  return size;
+}
+
+template <typename T>
+std::byte *copy(const T &src, std::byte *dst, size_t count) {
+  if constexpr (std::is_pointer_v<T>) {
+    std::memcpy(dst, src, count);
+  } else {
+    std::memcpy(dst, &src, count);
+  }
+  return dst + count;
+}
+
+void RequestParameters::serialize(std::byte *data_out) const {
+  auto size = this->size();
+  std::string foo;
+  data_out = copy(size, data_out, sizeof(size_t));
+  for (const auto &[key, value] : parameters_) {
+    data_out = copy(value.index(), data_out, sizeof(size_t));
+    data_out = copy(key.size(), data_out, sizeof(size_t));
+    std::visit(
+      [&](const auto &param) {
+        using T = std::decay_t<decltype(param)>;
+        if constexpr (std::is_same_v<T, std::string>) {
+          data_out = copy(param.size(), data_out, sizeof(size_t));
+        } else {
+          data_out = copy(sizeof(param), data_out, sizeof(size_t));
+        }
+      },
+      value);
+  }
+  for (const auto &[key, value] : parameters_) {
+    data_out = copy(key.c_str(), data_out, key.size());
+    std::visit(
+      [&](const auto &param) {
+        using T = std::decay_t<decltype(param)>;
+        if constexpr (std::is_same_v<T, std::string>) {
+          data_out = copy(param.c_str(), data_out, param.size());
+        } else {
+          data_out = copy(param, data_out, sizeof(param));
+        }
+      },
+      value);
+  }
+}
+
+/**
+ * @brief This is used to turn a runtime index into a variant with the right
+ * type
+ *
+ * @tparam Ts: the types of the variant. Order matters!
+ * @param i index of the variant type to create
+ * @return std::variant<Ts...>
+ *
+ * https://www.reddit.com/r/cpp/comments/f8cbzs/comment/fimjm2f/?context=3
+ */
+template <typename... Ts>
+[[nodiscard]] std::variant<Ts...> expand_type(std::size_t i) {
+  assert(i < sizeof...(Ts));
+  static constexpr auto table =
+    std::array{+[]() { return std::variant<Ts...>{Ts{}}; }...};
+  return table[i]();
+}
+
+void RequestParameters::deserialize(const std::byte *data_in) {
+  parameters_.clear();
+
+  auto size = std::to_integer<size_t>(*data_in);
+  data_in += sizeof(size_t);
+  std::vector<std::tuple<size_t, size_t, size_t>> params;
+  params.reserve(size);
+  for (auto i = 0U; i < size; i++) {
+    auto index = std::to_integer<size_t>(*data_in);
+    data_in += sizeof(size_t);
+    auto key_size = std::to_integer<size_t>(*data_in);
+    data_in += sizeof(size_t);
+    auto data_size = std::to_integer<size_t>(*data_in);
+    data_in += sizeof(size_t);
+    params.emplace_back(index, key_size, data_size);
+  }
+  for (const auto &[index, key_size, data_size] : params) {
+    std::string key;
+    key.resize(key_size);
+    std::memcpy(key.data(), data_in, key_size);
+    data_in += key_size;
+    Parameter param = expand_type<bool, int32_t, double, std::string>(index);
+    const auto &n = data_size;
+    std::visit(
+      [&](auto &value) {
+        using T = std::decay_t<decltype(value)>;
+        if constexpr (std::is_same_v<T, std::string>) {
+          value.resize(n);
+          std::memcpy(value.data(), data_in, n);
+          data_in += n;
+        } else {
+          std::memcpy(&value, data_in, n);
+          data_in += n;
+        }
+      },
+      param);
+    parameters_.try_emplace(key, param);
+  }
+}
+
 void InferenceRequest::setCallback(Callback &&callback) {
-  callback_ = callback;
+  callback_ = std::move(callback);
 }
 
 void InferenceRequest::runCallbackOnce(const InferenceResponse &response) {
@@ -117,7 +240,6 @@ InferenceRequestInput::InferenceRequestInput(void *data,
                                              std::string name)
   : dataType_(dataType) {
   this->data_ = data;
-  this->shared_data_ = nullptr;
   this->shape_ = std::move(shape);
   this->name_ = std::move(name);
   this->parameters_ = std::make_unique<RequestParameters>();
@@ -125,7 +247,6 @@ InferenceRequestInput::InferenceRequestInput(void *data,
 
 InferenceRequestInput::InferenceRequestInput() : dataType_(DataType::UINT32) {
   this->data_ = nullptr;
-  this->shared_data_ = nullptr;
   this->name_ = "";
   this->parameters_ = std::make_unique<RequestParameters>();
 }
@@ -140,12 +261,12 @@ void InferenceRequestInput::setDatatype(DataType type) {
 
 void InferenceRequestInput::setData(void *buffer) { this->data_ = buffer; }
 
-void InferenceRequestInput::setData(std::shared_ptr<std::byte> buffer) {
+void InferenceRequestInput::setData(std::vector<std::byte> &&buffer) {
   this->shared_data_ = std::move(buffer);
 }
 
 bool InferenceRequestInput::sharedData() const {
-  return this->shared_data_ != nullptr;
+  return this->shared_data_.empty();
 }
 
 size_t InferenceRequestInput::getSize() const {
@@ -154,12 +275,90 @@ size_t InferenceRequestInput::getSize() const {
 }
 
 void *InferenceRequestInput::getData() const {
-  if (this->shared_data_ != nullptr) {
-    return this->shared_data_.get();
+  if (!this->shared_data_.empty()) {
+    return (void *)shared_data_.data();
   }
   return this->data_;
 }
 
+struct InferenceRequestInputSizes {
+  size_t name;
+  size_t shape;
+  size_t dataType;
+  size_t parameters;
+  size_t data;
+  size_t shared_data;
+};
+
+size_t InferenceRequestInput::serializeSize() const {
+  auto size = sizeof(InferenceRequestInputSizes);
+  size += name_.length();
+  size += shape_.size() * sizeof(uint64_t);
+  size += sizeof(uint8_t);
+  size += parameters_->serializeSize();
+  if (!shared_data_.empty()) {
+    size += shared_data_.size();
+  } else {
+    size += sizeof(data_);
+  }
+  return size;
+}
+
+void InferenceRequestInput::serialize(std::byte *data_out) const {
+  InferenceRequestInputSizes metadata{name_.length(),
+                                      shape_.size() * sizeof(uint64_t),
+                                      sizeof(uint8_t),
+                                      parameters_->serializeSize(),
+                                      0,
+                                      0};
+  if (!shared_data_.empty()) {
+    metadata.shared_data = this->getSize() * dataType_.size();
+  } else {
+    metadata.data = sizeof(data_);
+  }
+  data_out = copy(metadata, data_out, sizeof(InferenceRequestInputSizes));
+  data_out = copy(name_.c_str(), data_out, metadata.name);
+  data_out = copy(shape_.data(), data_out, metadata.shape);
+  data_out = copy(static_cast<uint8_t>(dataType_), data_out, metadata.dataType);
+  parameters_->serialize(data_out);
+  data_out += metadata.parameters;
+  if (!shared_data_.empty()) {
+    proteus::copy(shared_data_.data(), data_out, metadata.shared_data);
+  } else {
+    std::memcpy(data_out, &data_, metadata.data);
+  }
+}
+
+void InferenceRequestInput::deserialize(const std::byte *data_in) {
+  const auto metadata =
+    *reinterpret_cast<const InferenceRequestInputSizes *>(data_in);
+  data_in += sizeof(InferenceRequestInputSizes);
+
+  name_.resize(metadata.name);
+  std::memcpy(name_.data(), data_in, metadata.name);
+  data_in += metadata.name;
+
+  shape_.resize(metadata.shape / sizeof(uint64_t));
+  std::memcpy(shape_.data(), data_in, metadata.shape);
+  data_in += metadata.shape;
+
+  uint8_t type;
+  std::memcpy(&type, data_in, metadata.dataType);
+  data_in += metadata.dataType;
+  dataType_ = DataType(static_cast<DataType::Value>(type));
+
+  parameters_ = std::make_shared<RequestParameters>();
+  parameters_->deserialize(data_in);
+  data_in += parameters_->serializeSize();
+
+  if (metadata.shared_data != 0) {
+    shared_data_.resize(metadata.shared_data);
+    std::memcpy(shared_data_.data(), data_in, metadata.shared_data);
+  } else {
+    std::memcpy(&data_, data_in, metadata.data);
+  }
+}
+
 InferenceRequestOutput::InferenceRequestOutput() {
   this->data_ = nullptr;
   this->name_ = "";
diff --git a/src/proteus/core/worker_info.hpp b/src/proteus/core/worker_info.hpp
index fced6589e..740ee55e8 100644
--- a/src/proteus/core/worker_info.hpp
+++ b/src/proteus/core/worker_info.hpp
@@ -28,8 +28,8 @@
 #include <thread>   // for thread, thread::id
 #include <vector>   // for vector
 
-#include "proteus/helpers/declarations.hpp"  // for BufferPtr
-#include "proteus/helpers/queue.hpp"         // for BufferPtrsQueuePtr
+#include "proteus/declarations.hpp"  // for BufferPtr
+#include "proteus/util/queue.hpp"    // for BufferPtrsQueuePtr
 
 namespace proteus {
 class Batcher;
diff --git a/src/proteus/observation/tracing.hpp b/src/proteus/observation/tracing.hpp
index 363321ef4..b062e611c 100644
--- a/src/proteus/observation/tracing.hpp
+++ b/src/proteus/observation/tracing.hpp
@@ -23,9 +23,9 @@
 #include <memory>  // for shared_ptr, uniqu...
 #include <stack>   // for stack
 
-#include "proteus/build_options.hpp"         // for PROTEUS_ENABLE_TR...
-#include "proteus/core/predict_api.hpp"      // for RequestParameters
-#include "proteus/helpers/declarations.hpp"  // for StringMap
+#include "proteus/build_options.hpp"     // for PROTEUS_ENABLE_TR...
+#include "proteus/core/predict_api.hpp"  // for RequestParameters
+#include "proteus/declarations.hpp"      // for StringMap
 
 // IWYU pragma: no_forward_declare proteus::RequestParameters
 
diff --git a/src/proteus/servers/grpc_server.cpp b/src/proteus/servers/grpc_server.cpp
index 418de0595..8aea158c1 100644
--- a/src/proteus/servers/grpc_server.cpp
+++ b/src/proteus/servers/grpc_server.cpp
@@ -46,10 +46,11 @@
 #include "proteus/core/exceptions.hpp"            // for invalid_argument
 #include "proteus/core/interface.hpp"             // for Interface, Interfac...
 #include "proteus/core/predict_api_internal.hpp"  // for InferenceRequestInput
-#include "proteus/helpers/declarations.hpp"       // for BufferRawPtrs, Infe...
-#include "proteus/helpers/string.hpp"             // for toLower
+#include "proteus/declarations.hpp"               // for BufferRawPtrs, Infe...
 #include "proteus/observation/logging.hpp"        // for Logger, Loggers
 #include "proteus/observation/tracing.hpp"        // for Trace, startTrace
+#include "proteus/util/string.hpp"                // for toLower
+#include "proteus/util/traits.hpp"                // for is_any
 
 namespace proteus {
 class CallDataModelInfer;
@@ -198,6 +199,26 @@ class CallDataServerStream : public CallData<RequestType, ReplyType> {
   ::grpc::ServerAsyncWriter<ReplyType> responder_;
 };
 
+struct WriteData {
+  template <typename T, typename Tensor>
+  void operator()(Buffer* buffer, Tensor* tensor, size_t offset,
+                  size_t size) const {
+    auto* contents = getTensorContents<T>(tensor);
+    if constexpr (util::is_any_v<T, bool, uint32_t, uint64_t, int32_t, int64_t,
+                                 float, double, char>) {
+      auto* dest = static_cast<std::byte*>(buffer->data(offset));
+      std::memcpy(dest, contents, size * sizeof(T));
+    } else if constexpr (util::is_any_v<T, uint8_t, uint16_t, int8_t,
+                                        int16_t>) {
+      for (size_t i = 0; i < size; i++) {
+        offset = buffer->write(static_cast<T>(contents[i]), offset);
+      }
+    } else {
+      static_assert(!sizeof(T), "Invalid type to WriteData");
+    }
+  }
+};
+
 template <>
 class InferenceRequestInputBuilder<
   inference::ModelInferRequest_InferInputTensor> {
@@ -206,7 +227,6 @@ class InferenceRequestInputBuilder<
     const inference::ModelInferRequest_InferInputTensor& req,
     Buffer* input_buffer, size_t offset) {
     InferenceRequestInput input;
-    input.shared_data_ = nullptr;
     input.name_ = req.name();
     input.shape_.reserve(req.shape_size());
     for (const auto& index : req.shape()) {
@@ -219,82 +239,8 @@ class InferenceRequestInputBuilder<
     auto size = input.getSize();
     auto* dest = static_cast<std::byte*>(input_buffer->data()) + offset;
 
-    const auto tensor = req.contents();
-    switch (input.getDatatype()) {
-      case DataType::BOOL: {
-        std::memcpy(dest, tensor.bool_contents().data(), size * sizeof(char));
-        break;
-      }
-      case DataType::UINT8: {
-        const auto* value = tensor.uint_contents().data();
-        for (size_t i = 0; i < size; i++) {
-          dest[i] = static_cast<std::byte>(value[i]);
-        }
-        break;
-      }
-      case DataType::UINT16: {
-        const auto* value = tensor.uint_contents().data();
-        for (size_t i = 0; i < size; i++) {
-          std::memcpy(dest + i, value + i, sizeof(uint16_t));
-        }
-        break;
-      }
-      case DataType::UINT32: {
-        const auto value = tensor.uint_contents().data();
-        std::memcpy(dest, value, size * sizeof(uint32_t));
-        break;
-      }
-      case DataType::UINT64: {
-        std::memcpy(dest, tensor.uint64_contents().data(),
-                    size * sizeof(uint64_t));
-        break;
-      }
-      case DataType::INT8: {
-        const auto* value = tensor.int_contents().data();
-        for (size_t i = 0; i < size; i++) {
-          dest[i] = static_cast<std::byte>(value[i]);
-        }
-        break;
-      }
-      case DataType::INT16: {
-        const auto* value = tensor.int_contents().data();
-        for (size_t i = 0; i < size; i++) {
-          std::memcpy(dest + i, value + i, sizeof(int16_t));
-        }
-        break;
-      }
-      case DataType::INT32: {
-        std::memcpy(dest, tensor.int_contents().data(), size * sizeof(int32_t));
-        break;
-      }
-      case DataType::INT64: {
-        std::memcpy(dest, tensor.int64_contents().data(),
-                    size * sizeof(int64_t));
-        break;
-      }
-      case DataType::FP16: {
-        // FIXME(varunsh): this is not handled
-        std::cout << "Writing FP16 not supported\n";
-        break;
-      }
-      case DataType::FP32: {
-        std::memcpy(dest, tensor.fp32_contents().data(), size * sizeof(float));
-        break;
-      }
-      case DataType::FP64: {
-        std::memcpy(dest, tensor.fp64_contents().data(), size * sizeof(double));
-        break;
-      }
-      case DataType::STRING: {
-        std::memcpy(dest, tensor.bytes_contents().data(),
-                    size * sizeof(std::byte));
-        break;
-      }
-      default:
-        // TODO(varunsh): what should we do here?
-        std::cout << "Unknown datatype\n";
-        break;
-    }
+    switchOverTypes(WriteData(), input.getDatatype(), input_buffer, &req,
+                    offset, size);
 
     input.data_ = dest;
     return input;
@@ -442,7 +388,7 @@ void grpcUnaryCallback(CallDataModelInfer* calldata,
     return;
   }
   try {
-    mapResponsetoProto(response, calldata->getReply());
+    mapResponseToProto(response, calldata->getReply());
   } catch (const invalid_argument& e) {
     calldata->finish(::grpc::Status(StatusCode::UNKNOWN, e.what()));
     return;
@@ -567,7 +513,7 @@ CALLDATA_IMPL(ModelLoad, Unary) {
   auto parameters = mapProtoToParameters(request_.parameters());
 
   auto* model = request_.mutable_name();
-  toLower(model);
+  util::toLower(model);
   try {
     ::proteus::modelLoad(*model, parameters.get());
   } catch (const runtime_error& e) {
@@ -585,7 +531,7 @@ CALLDATA_IMPL_END
 
 CALLDATA_IMPL(ModelUnload, Unary) {
   auto* model = request_.mutable_name();
-  toLower(model);
+  util::toLower(model);
   ::proteus::modelUnload(*model);
   finish();
 }
@@ -595,7 +541,7 @@ CALLDATA_IMPL(WorkerLoad, Unary) {
   auto parameters = mapProtoToParameters(request_.parameters());
 
   auto* model = request_.mutable_name();
-  toLower(model);
+  util::toLower(model);
 
   try {
     auto endpoint = ::proteus::workerLoad(*model, parameters.get());
@@ -613,7 +559,7 @@ CALLDATA_IMPL_END
 
 CALLDATA_IMPL(WorkerUnload, Unary) {
   auto* worker = request_.mutable_name();
-  toLower(worker);
+  util::toLower(worker);
   ::proteus::workerUnload(*worker);
   finish();
 }
diff --git a/src/proteus/servers/http_server.cpp b/src/proteus/servers/http_server.cpp
index d7b46d0fd..4e1c1acc9 100644
--- a/src/proteus/servers/http_server.cpp
+++ b/src/proteus/servers/http_server.cpp
@@ -37,11 +37,11 @@
 #include "proteus/core/exceptions.hpp"            // for runtime_error, inva...
 #include "proteus/core/interface.hpp"             // for Interface
 #include "proteus/core/predict_api_internal.hpp"  // for RequestParametersPtr
-#include "proteus/helpers/string.hpp"             // for toLower
 #include "proteus/observation/logging.hpp"        // for Logger, PROTEUS_LOG...
 #include "proteus/observation/metrics.hpp"        // for Metrics, MetricCoun...
 #include "proteus/observation/tracing.hpp"        // for startTrace, Trace
 #include "proteus/servers/websocket_server.hpp"   // for WebsocketServer
+#include "proteus/util/string.hpp"                // for toLower
 
 using drogon::HttpRequestPtr;
 using drogon::HttpResponse;
@@ -279,7 +279,7 @@ void v2::ProteusHttpServer::modelLoad(
   const HttpRequestPtr &req,
   std::function<void(const HttpResponsePtr &)> &&callback,
   const std::string &model) const {
-  auto model_lower = toLower(model);
+  auto model_lower = util::toLower(model);
 #ifdef PROTEUS_ENABLE_TRACING
   auto trace = startTrace(&(__func__[0]), req->getHeaders());
   trace->setAttribute("model", model_lower);
@@ -326,7 +326,7 @@ void v2::ProteusHttpServer::modelUnload(
   auto trace = startTrace(&(__func__[0]), req->getHeaders());
 #endif
 
-  auto model_lower = toLower(model);
+  auto model_lower = util::toLower(model);
 
 #ifdef PROTEUS_ENABLE_TRACING
   trace->setAttribute("model", model_lower);
@@ -359,7 +359,7 @@ void v2::ProteusHttpServer::workerLoad(
     parameters = std::make_unique<RequestParameters>();
   }
 
-  auto worker_lower = toLower(worker);
+  auto worker_lower = util::toLower(worker);
 
 #ifdef PROTEUS_ENABLE_TRACING
   trace->setAttribute("model", worker_lower);
@@ -394,7 +394,7 @@ void v2::ProteusHttpServer::workerUnload(
   auto trace = startTrace(&(__func__[0]), req->getHeaders());
 #endif
 
-  auto worker_lower = toLower(worker);
+  auto worker_lower = util::toLower(worker);
 
   PROTEUS_LOG_INFO(logger_, "Received unload request is for " + worker_lower);
 
diff --git a/src/proteus/servers/http_server.hpp b/src/proteus/servers/http_server.hpp
index 80b7565df..e14ed439f 100644
--- a/src/proteus/servers/http_server.hpp
+++ b/src/proteus/servers/http_server.hpp
@@ -68,7 +68,7 @@ class ProteusHttpServer
   ADD_METHOD_TO(ProteusHttpServer::getModelMetadata, "v2/models/{model}",
                 drogon::Get, drogon::Options);
   /// Register the getHardware endpoint
-  ADD_METHOD_TO(ProteusHttpServer::hasHardware, "v2/hardware", drogon::Get,
+  ADD_METHOD_TO(ProteusHttpServer::hasHardware, "v2/hardware", drogon::Post,
                 drogon::Options);
   /// Register the modelList endpoint
   ADD_METHOD_TO(ProteusHttpServer::modelList, "v2/models", drogon::Get,
diff --git a/src/proteus/servers/websocket_server.cpp b/src/proteus/servers/websocket_server.cpp
index 52837e0ab..063fc34b4 100644
--- a/src/proteus/servers/websocket_server.cpp
+++ b/src/proteus/servers/websocket_server.cpp
@@ -139,10 +139,14 @@ std::shared_ptr<InferenceRequest> DrogonWs::getRequest(
       this->json_, input_buffers, input_offset, output_buffers, output_offset);
     Callback callback =
       [conn = std::move(this->conn_)](const InferenceResponse &response) {
-        auto outputs = response.getOutputs();
-        auto *msg = static_cast<std::string *>(outputs[0].getData());
-        if (conn->connected()) {
-          conn->send(*msg);
+        if (response.isError()) {
+          conn->send(response.getError());
+        } else {
+          auto outputs = response.getOutputs();
+          const auto *msg = static_cast<char *>(outputs[0].getData());
+          if (conn->connected()) {
+            conn->send(msg, outputs[0].getSize());
+          }
         }
       };
     request->setCallback(std::move(callback));
diff --git a/src/proteus/servers/websocket_server.hpp b/src/proteus/servers/websocket_server.hpp
index 6e10d8264..99b758c43 100644
--- a/src/proteus/servers/websocket_server.hpp
+++ b/src/proteus/servers/websocket_server.hpp
@@ -26,10 +26,10 @@
 #include <string>     // for string
 #include <vector>     // for vector
 
-#include "proteus/build_options.hpp"         // for PROTEUS_ENABLE_LOGGING
-#include "proteus/core/interface.hpp"        // for Interface
-#include "proteus/helpers/declarations.hpp"  // for BufferRawPtrs
-#include "proteus/observation/logging.hpp"   // for LoggerPtr
+#include "proteus/build_options.hpp"        // for PROTEUS_ENABLE_LOGGING
+#include "proteus/core/interface.hpp"       // for Interface
+#include "proteus/declarations.hpp"         // for BufferRawPtrs
+#include "proteus/observation/logging.hpp"  // for LoggerPtr
 
 namespace Json {
 class Value;
diff --git a/src/proteus/helpers/CMakeLists.txt b/src/proteus/util/CMakeLists.txt
similarity index 73%
rename from src/proteus/helpers/CMakeLists.txt
rename to src/proteus/util/CMakeLists.txt
index 9e8b60d07..adf3167df 100644
--- a/src/proteus/helpers/CMakeLists.txt
+++ b/src/proteus/util/CMakeLists.txt
@@ -18,20 +18,22 @@ list(
          compression
          parse_env
          exec
+         read_nth_line
 )
 
+list(APPEND UTIL_OBJS)
+
 foreach(target ${targets})
   add_library(${target} OBJECT ${target}.cpp)
-  target_include_directories(${target} PRIVATE ${PROTEUS_INCLUDE_DIRS})
   enable_ipo_on_target(${target})
+  target_include_directories(${target} PRIVATE ${PROJECT_SOURCE_DIR}/src)
 
-  list(APPEND HELPER_OBJS $<TARGET_OBJECTS:${target}>)
+  list(APPEND UTIL_OBJS $<TARGET_OBJECTS:${target}>)
 endforeach()
 
 target_link_libraries(base64 INTERFACE b64)
 target_link_libraries(compression INTERFACE z)
 
-add_library(helpers STATIC ${HELPER_OBJS})
-target_link_libraries(helpers INTERFACE ${targets})
-
-set(HELPER_OBJS ${HELPER_OBJS} PARENT_SCOPE)
+add_library(util SHARED ${UTIL_OBJS})
+target_link_libraries(util PRIVATE ${targets})
+set_target_properties(util PROPERTIES OUTPUT_NAME proteus-util)
diff --git a/src/proteus/helpers/base64.cpp b/src/proteus/util/base64.cpp
similarity index 96%
rename from src/proteus/helpers/base64.cpp
rename to src/proteus/util/base64.cpp
index 6b985ac8b..db32fcf44 100644
--- a/src/proteus/helpers/base64.cpp
+++ b/src/proteus/util/base64.cpp
@@ -17,7 +17,7 @@
  * @brief Implements base64 encoding/decoding
  */
 
-#include "proteus/helpers/base64.hpp"
+#include "proteus/util/base64.hpp"
 
 #include <stdexcept>
 
@@ -27,7 +27,7 @@
 #include <b64/decode.h>
 #include <b64/encode.h>
 
-namespace proteus {
+namespace proteus::util {
 
 constexpr size_t minDecodeLength(size_t length) { return (length * 3 + 3) / 4; }
 
@@ -84,4 +84,4 @@ std::string base64_encode(const char* in, size_t in_length) {
   return s;
 }
 
-}  // namespace proteus
+}  // namespace proteus::util
diff --git a/src/proteus/helpers/base64.hpp b/src/proteus/util/base64.hpp
similarity index 90%
rename from src/proteus/helpers/base64.hpp
rename to src/proteus/util/base64.hpp
index c03715c05..793704db9 100644
--- a/src/proteus/helpers/base64.hpp
+++ b/src/proteus/util/base64.hpp
@@ -17,13 +17,13 @@
  * @brief Defines base64 encoding/decoding
  */
 
-#ifndef GUARD_PROTEUS_HELPERS_BASE64
-#define GUARD_PROTEUS_HELPERS_BASE64
+#ifndef GUARD_PROTEUS_UTIL_BASE64
+#define GUARD_PROTEUS_UTIL_BASE64
 
 #include <cstddef>  // for size_t
 #include <string>   // for string
 
-namespace proteus {
+namespace proteus::util {
 
 /**
  * @brief Decodes a base64-encoded string and returns it
@@ -58,6 +58,6 @@ std::string base64_encode(std::string in);
  * @return std::string encoded string
  */
 std::string base64_encode(const char* in, size_t in_len);
-}  // namespace proteus
+}  // namespace proteus::util
 
-#endif  // GUARD_PROTEUS_HELPERS_BASE64
+#endif  // GUARD_PROTEUS_UTIL_BASE64
diff --git a/src/proteus/helpers/compression.cpp b/src/proteus/util/compression.cpp
similarity index 94%
rename from src/proteus/helpers/compression.cpp
rename to src/proteus/util/compression.cpp
index cc016d88b..6e066ae69 100644
--- a/src/proteus/helpers/compression.cpp
+++ b/src/proteus/util/compression.cpp
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "proteus/helpers/compression.hpp"
+#include "proteus/util/compression.hpp"
 
 #include <zlib.h>  // for z_stream, inflate, inflateEnd, Z_OK, Z_NO_FLUSH
 
 #include <array>    // for array
 #include <cstring>  // for memset
 
+namespace proteus::util {
+
 std::string z_decompress(const char *str, int len) {
   constexpr auto buffer_size = 32768;
 
@@ -47,3 +49,5 @@ std::string z_decompress(const char *str, int len) {
   }
   return "";
 }
+
+}  // namespace proteus::util
diff --git a/src/proteus/helpers/compression.hpp b/src/proteus/util/compression.hpp
similarity index 94%
rename from src/proteus/helpers/compression.hpp
rename to src/proteus/util/compression.hpp
index 3cd409932..ad626f0e8 100644
--- a/src/proteus/helpers/compression.hpp
+++ b/src/proteus/util/compression.hpp
@@ -17,6 +17,8 @@
 
 #include <string>
 
+namespace proteus::util {
+
 /**
  * @brief Inflate a deflated (i.e. compressed) argument with zlib. Taken from
  * https://stackoverflow.com/a/50082615
@@ -27,4 +29,6 @@
  */
 std::string z_decompress(const char *str, int len);
 
+}  // namespace proteus::util
+
 #endif  // GUARD_PROTEUS_HELPERS_COMPRESSION
diff --git a/src/proteus/helpers/ctpl.h b/src/proteus/util/ctpl.h
similarity index 99%
rename from src/proteus/helpers/ctpl.h
rename to src/proteus/util/ctpl.h
index 748ae0495..b05e4db8d 100644
--- a/src/proteus/helpers/ctpl.h
+++ b/src/proteus/util/ctpl.h
@@ -32,7 +32,7 @@
 #include <utility>
 #include <vector>
 
-#include "proteus/helpers/thread.hpp"
+#include "proteus/util/thread.hpp"
 
 #ifndef _ctplThreadPoolLength_
 #define _ctplThreadPoolLength_ 100
@@ -195,7 +195,7 @@ class thread_pool {
     std::shared_ptr<std::atomic<bool>> flag(
       this->flags[i]);  // a copy of the shared ptr to the flag
     auto f = [this, i, flag /* a copy of the shared ptr to the flag */]() {
-      proteus::setThreadName("ctplPool");
+      proteus::util::setThreadName("ctplPool");
       std::atomic<bool> &_flag = *flag;
       std::function<void(int id)> *_f;
       bool isPop = this->q.try_dequeue(_f);
diff --git a/src/proteus/helpers/exec.cpp b/src/proteus/util/exec.cpp
similarity index 71%
rename from src/proteus/helpers/exec.cpp
rename to src/proteus/util/exec.cpp
index 53910c541..1de7a471f 100644
--- a/src/proteus/helpers/exec.cpp
+++ b/src/proteus/util/exec.cpp
@@ -12,16 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "proteus/helpers/exec.hpp"
+#include "proteus/util/exec.hpp"
 
-#include <array>   // for array
-#include <cstdio>  // for pclose, fgets, popen, FILE
-#include <memory>  // for unique_ptr
-#include <string>  // for string
+#include <array>         // for array
+#include <cstdio>        // for pclose, fgets, popen, FILE
+#include <memory>        // for unique_ptr
+#include <string>        // for string
+#include <system_error>  // for system_error
 
-#include "proteus/core/exceptions.hpp"
-
-namespace proteus {
+namespace proteus::util {
 
 // TODO(varunsh): avoid popen
 std::string exec(const char* cmd) {
@@ -30,7 +29,7 @@ std::string exec(const char* cmd) {
   // NOLINTNEXTLINE(cert-env33-c)
   std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd, "r"), &pclose);
   if (!pipe) {
-    throw external_error("popen() failed!");
+    throw std::system_error(EPIPE, std::generic_category(), "popen failed");
   }
   while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
     result += buffer.data();
@@ -38,4 +37,4 @@ std::string exec(const char* cmd) {
   return result;
 }
 
-}  // namespace proteus
+}  // namespace proteus::util
diff --git a/src/proteus/helpers/exec.hpp b/src/proteus/util/exec.hpp
similarity index 84%
rename from src/proteus/helpers/exec.hpp
rename to src/proteus/util/exec.hpp
index c59187523..b457e1034 100644
--- a/src/proteus/helpers/exec.hpp
+++ b/src/proteus/util/exec.hpp
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef GUARD_PROTEUS_HELPERS_EXEC
-#define GUARD_PROTEUS_HELPERS_EXEC
+#ifndef GUARD_PROTEUS_UTIL_EXEC
+#define GUARD_PROTEUS_UTIL_EXEC
 
 #include <string>
 
-namespace proteus {
+namespace proteus::util {
 
 /**
  * @brief Execute an arbitrary command on the command line.
@@ -28,6 +28,6 @@ namespace proteus {
  */
 std::string exec(const char* cmd);
 
-}  // namespace proteus
+}  // namespace proteus::util
 
-#endif  // GUARD_PROTEUS_HELPERS_EXEC
+#endif  // GUARD_PROTEUS_UTIL_EXEC
diff --git a/src/proteus/helpers/parse_env.cpp b/src/proteus/util/parse_env.cpp
similarity index 93%
rename from src/proteus/helpers/parse_env.cpp
rename to src/proteus/util/parse_env.cpp
index 8618d8b00..5fed92f23 100644
--- a/src/proteus/helpers/parse_env.cpp
+++ b/src/proteus/util/parse_env.cpp
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "proteus/helpers/parse_env.hpp"
+#include "proteus/util/parse_env.hpp"
 
 #include <cstdlib>  // for getenv
 #include <regex>    // for match_results<>::_Base_type, match_results, rege...
@@ -21,7 +21,7 @@
 
 // https://stackoverflow.com/a/23442780
 
-namespace proteus {
+namespace proteus::util {
 
 // Update the input string.
 void autoExpandEnvironmentVariables(std::string& text) {
@@ -41,4 +41,4 @@ std::string expandEnvironmentVariables(const std::string& input) {
   return text;
 }
 
-}  // namespace proteus
+}  // namespace proteus::util
diff --git a/src/proteus/helpers/parse_env.hpp b/src/proteus/util/parse_env.hpp
similarity index 94%
rename from src/proteus/helpers/parse_env.hpp
rename to src/proteus/util/parse_env.hpp
index aa7431dd9..e8b1f60fd 100644
--- a/src/proteus/helpers/parse_env.hpp
+++ b/src/proteus/util/parse_env.hpp
@@ -17,7 +17,7 @@
 
 #include <string>
 
-namespace proteus {
+namespace proteus::util {
 
 /// Expand any environment variables in the string in place
 void autoExpandEnvironmentVariables(std::string& text);
@@ -25,6 +25,6 @@ void autoExpandEnvironmentVariables(std::string& text);
 // Expand any environment variables in the string and return a copy
 std::string expandEnvironmentVariables(const std::string& input);
 
-}  // namespace proteus
+}  // namespace proteus::util
 
 #endif  // GUARD_PROTEUS_HELPERS_PARSE_ENV
diff --git a/src/proteus/util/pre_post/center_crop.hpp b/src/proteus/util/pre_post/center_crop.hpp
new file mode 100644
index 000000000..bcfaa9c64
--- /dev/null
+++ b/src/proteus/util/pre_post/center_crop.hpp
@@ -0,0 +1,41 @@
+// Copyright 2022 Xilinx Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef GUARD_PROTEUS_UTIL_PRE_POST_CENTER_CROP
+#define GUARD_PROTEUS_UTIL_PRE_POST_CENTER_CROP
+
+#include <opencv2/core.hpp>  // for cv::Mat
+
+namespace proteus::util {
+
+/**
+ * @brief Image preprocessing helper.  Crops the input `img` from center to a
+ * specific dimension specified
+ *
+ * @param img: Image which needs to be cropped
+ * @param height: height of the output image
+ * @param width: width of the output image
+ * @return cv::Mat: Image cropped to required shape
+ */
+cv::Mat center_crop(cv::Mat img, int height, int width) {
+  const int offsetW = (img.cols - width) / 2;
+  const int offsetH = (img.rows - height) / 2;
+  const cv::Rect roi(offsetW, offsetH, width, height);
+  img = img(roi);
+  return img;
+}
+
+}  // namespace proteus::util
+
+#endif  // GUARD_PROTEUS_UTIL_PRE_POST_CENTER_CROP
diff --git a/src/proteus/util/pre_post/get_top_k.hpp b/src/proteus/util/pre_post/get_top_k.hpp
new file mode 100644
index 000000000..bc1e086b8
--- /dev/null
+++ b/src/proteus/util/pre_post/get_top_k.hpp
@@ -0,0 +1,50 @@
+// Copyright 2022 Xilinx Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef GUARD_PROTEUS_UTIL_PRE_POST_GET_TOP_K
+#define GUARD_PROTEUS_UTIL_PRE_POST_GET_TOP_K
+
+#include <queue>
+#include <vector>
+
+namespace proteus::util {
+
+/**
+ * @brief After running softmax, get the labels associated with the top k values
+ *
+ * @param d pointer to the data
+ * @param size number of elements in the data
+ * @param k number of top elements to return
+ * @return std::vector<int>
+ */
+std::vector<int> get_top_k(const double* d, size_t size, int k) {
+  std::priority_queue<std::pair<float, int>> q;
+
+  for (auto i = 0U; i < size; ++i) {
+    q.push(std::pair<float, int>(d[i], i));
+  }
+
+  std::vector<int> topKIndex;
+  topKIndex.reserve(k);
+  for (auto i = 0; i < k; ++i) {
+    const auto& [prob, index] = q.top();
+    topKIndex.push_back(index);
+    q.pop();
+  }
+  return topKIndex;
+}
+
+}  // namespace proteus::util
+
+#endif  // GUARD_PROTEUS_UTIL_PRE_POST_GET_TOP_K
diff --git a/src/proteus/util/pre_post/image_preprocess.hpp b/src/proteus/util/pre_post/image_preprocess.hpp
new file mode 100644
index 000000000..c770d646e
--- /dev/null
+++ b/src/proteus/util/pre_post/image_preprocess.hpp
@@ -0,0 +1,146 @@
+// Copyright 2022 Xilinx Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef GUARD_PROTEUS_UTIL_PRE_POST_IMAGE_PREPROCESS
+#define GUARD_PROTEUS_UTIL_PRE_POST_IMAGE_PREPROCESS
+
+#include <array>
+#include <cassert>
+#include <cstdint>
+#include <opencv2/core.hpp>       // for Mat, Vec3b, MatSize, Vec, CV_8SC3
+#include <opencv2/imgcodecs.hpp>  // for imread
+#include <opencv2/imgproc.hpp>    // for resize
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+namespace proteus::util {
+
+enum class ImageOrder {
+  NHWC,
+  NCHW,
+};
+
+template <typename T, int C>
+struct ImagePreprocessOptions {
+  int height = 224;
+  int width = 224;
+  int channels = C;
+
+  bool convert_color = false;
+  cv::ColorConversionCodes color_code;
+
+  bool convert_type = false;
+  int type = 0;
+  double convert_scale = 0;
+
+  bool normalize = false;
+  ImageOrder order = ImageOrder::NHWC;
+  std::array<T, C> mean;
+  std::array<T, C> std;
+
+  bool assign = false;
+};
+
+namespace detail {
+
+template <typename T, typename F>
+void nestedLoop(int a, int b, int c, T* output, F f) {
+  for (int i = 0; i < a; i++) {
+    for (int j = 0; j < b; j++) {
+      for (int k = 0; k < c; k++) {
+        auto output_index = (i * b * c) + (j * c) + k;
+        output[output_index] = static_cast<T>(f(i, j, k));
+      }
+    }
+  }
+}
+
+template <typename T, int C>
+void normalize(const cv::Mat& img, ImageOrder order, T* output, const T* mean,
+               const T* std) {
+  auto height = img.size[0];
+  auto width = img.size[1];
+  switch (order) {
+    case ImageOrder::NHWC:
+      nestedLoop(height, width, C, output, [&](int h, int w, int c) -> T {
+        return (static_cast<T>(img.at<cv::Vec<T, C>>(h, w)[c]) - mean[c]) *
+               std[c];
+      });
+      break;
+    case ImageOrder::NCHW:
+      nestedLoop(C, height, width, output, [&](int c, int h, int w) {
+        return (static_cast<T>(img.at<cv::Vec<T, C>>(h, w)[c]) - mean[c]) *
+               std[c];
+      });
+      break;
+    default:
+      throw std::invalid_argument("Unknown image order");
+  }
+}
+
+}  // namespace detail
+
+template <typename T>
+std::vector<std::vector<T>> imagePreprocess(
+  const std::vector<std::string>& paths,
+  const ImagePreprocessOptions<T, 3>& options) {
+  std::vector<std::vector<T>> outputs;
+  outputs.reserve(paths.size());
+
+  const auto& height = options.height;
+  const auto& width = options.width;
+  const auto& channels = options.channels;
+  const auto& mean = options.mean;
+  const auto& std = options.std;
+
+  constexpr auto C = 3;
+  assert(channels == C);
+
+  auto index = 0;
+  for (const auto& path : paths) {
+    auto img = cv::imread(path);
+    if (img.empty()) {
+      throw std::runtime_error(std::string("Unable to load image ") + path);
+    }
+    if (options.convert_color) {
+      cv::cvtColor(img, img, options.color_code);
+    }
+
+    cv::resize(img, img, cv::Size(width, height), cv::INTER_LINEAR);
+    if (options.convert_type) {
+      img.convertTo(img, options.type, options.convert_scale);
+    }
+    img = img.isContinuous() ? img : img.clone();
+
+    auto size = img.size[0] * img.size[1] * channels;
+    outputs.emplace_back();
+    auto& output = outputs[index++];
+    output.resize(size);
+
+    if (options.normalize) {
+      detail::normalize<T, C>(img, options.order, output.data(), mean.data(),
+                              std.data());
+    }
+
+    if (options.assign) {
+      output.assign(img.data, img.data + size);
+    }
+  }
+  return outputs;
+}
+
+}  // namespace proteus::util
+
+#endif  // GUARD_PROTEUS_UTIL_PRE_POST_IMAGE_PREPROCESS
diff --git a/src/proteus/util/pre_post/resnet50_postprocess.hpp b/src/proteus/util/pre_post/resnet50_postprocess.hpp
new file mode 100644
index 000000000..f535863ca
--- /dev/null
+++ b/src/proteus/util/pre_post/resnet50_postprocess.hpp
@@ -0,0 +1,44 @@
+// Copyright 2022 Xilinx Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef GUARD_PROTEUS_UTIL_PRE_POST_RESNET50_POSTPROCESS
+#define GUARD_PROTEUS_UTIL_PRE_POST_RESNET50_POSTPROCESS
+
+#include <vector>
+
+#include "proteus/util/pre_post/get_top_k.hpp"
+#include "proteus/util/pre_post/softmax.hpp"
+
+namespace proteus::util {
+
+/**
+ * @brief Perform postprocessing of the data
+ *
+ * @tparam T the expected type of the data
+ * @param output output from Proteus
+ * @param k number of top elements to return
+ * @return std::vector<int>
+ */
+template <typename T>
+std::vector<int> resnet50Postprocess(const T* data, size_t size, int k) {
+  std::vector<double> softmax;
+  softmax.resize(size, 0);
+
+  calc_softmax(data, size, softmax.data());
+  return get_top_k(softmax.data(), size, k);
+}
+
+}  // namespace proteus::util
+
+#endif  // GUARD_PROTEUS_UTIL_PRE_POST_RESNET50_POSTPROCESS
diff --git a/src/proteus/util/pre_post/softmax.hpp b/src/proteus/util/pre_post/softmax.hpp
new file mode 100644
index 000000000..edce9c8f3
--- /dev/null
+++ b/src/proteus/util/pre_post/softmax.hpp
@@ -0,0 +1,53 @@
+// Copyright 2022 Xilinx Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef GUARD_PROTEUS_UTIL_PRE_POST_SOFTMAX
+#define GUARD_PROTEUS_UTIL_PRE_POST_SOFTMAX
+
+#include <cstddef>
+
+namespace proteus::util {
+
+/**
+ * @brief Calculate softmax of the data
+ *
+ * @tparam T the expected type of the data
+ * @param data pointer to the raw data
+ * @param size number of elements in the raw data
+ * @param result pointer to store the computed results
+ */
+template <typename T>
+void calc_softmax(const T* data, size_t size, double* result) {
+  double sum = 0;
+
+  auto max = data[0];
+  for (size_t i = 1; i < size; i++) {
+    if (data[i] > max) {
+      max = data[i];
+    }
+  }
+
+  for (size_t i = 0; i < size; i++) {
+    result[i] = exp(data[i] - max);
+    sum += result[i];
+  }
+
+  for (size_t i = 0; i < size; i++) {
+    result[i] /= sum;
+  }
+}
+
+}  // namespace proteus::util
+
+#endif  // GUARD_PROTEUS_UTIL_PRE_POST_SOFTMAX
diff --git a/src/proteus/helpers/queue.hpp b/src/proteus/util/queue.hpp
similarity index 86%
rename from src/proteus/helpers/queue.hpp
rename to src/proteus/util/queue.hpp
index d78c750b9..f893ffca1 100644
--- a/src/proteus/helpers/queue.hpp
+++ b/src/proteus/util/queue.hpp
@@ -12,15 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef GUARD_PROTEUS_HELPERS_QUEUE
-#define GUARD_PROTEUS_HELPERS_QUEUE
+#ifndef GUARD_PROTEUS_UTIL_QUEUE
+#define GUARD_PROTEUS_UTIL_QUEUE
 
 #include <concurrentqueue/blockingconcurrentqueue.h>  // IWYU pragma: export
 
 #include <memory>
 #include <vector>
 
-#include "proteus/helpers/declarations.hpp"
+#include "proteus/declarations.hpp"
 
 namespace proteus {
 
@@ -34,4 +34,4 @@ using InferenceRequestPtrQueue = BlockingQueue<InferenceRequestPtr>;
 
 }  // namespace proteus
 
-#endif  // GUARD_PROTEUS_HELPERS_QUEUE
+#endif  // GUARD_PROTEUS_UTIL_QUEUE
diff --git a/src/proteus/util/read_nth_line.cpp b/src/proteus/util/read_nth_line.cpp
new file mode 100644
index 000000000..fcff98037
--- /dev/null
+++ b/src/proteus/util/read_nth_line.cpp
@@ -0,0 +1,42 @@
+// Copyright 2022 Advanced Micro Devices, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef GUARD_PROTEUS_UTIL_READ_NTH_LINE
+#define GUARD_PROTEUS_UTIL_READ_NTH_LINE
+
+#include "proteus/util/read_nth_line.hpp"
+
+#include <fstream>  // for ifstream
+
+namespace proteus::util {
+
+std::string readNthLine(const std::string& filename, int N) {
+  std::ifstream in(filename);
+  std::string line;
+  // for performance, reserve some initial space in the string
+  const auto kDefaultLineLength = 100;
+  line.reserve(kDefaultLineLength);
+
+  // skip N lines
+  for (int i = 0; i < N; ++i) {
+    std::getline(in, line);
+  }
+
+  std::getline(in, line);
+  return line;
+}
+
+}  // namespace proteus::util
+
+#endif  // GUARD_PROTEUS_UTIL_READ_NTH_LINE
diff --git a/src/proteus/util/read_nth_line.hpp b/src/proteus/util/read_nth_line.hpp
new file mode 100644
index 000000000..dfb84c098
--- /dev/null
+++ b/src/proteus/util/read_nth_line.hpp
@@ -0,0 +1,33 @@
+// Copyright 2022 Advanced Micro Devices, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef GUARD_PROTEUS_UTIL_READ_NTH_LINE
+#define GUARD_PROTEUS_UTIL_READ_NTH_LINE
+
+#include <string>
+
+namespace proteus::util {
+
+/**
+ * @brief This method will read the class file and returns class name
+ *
+ * @param filename: The location of the file containing class names
+ * @param n: Nth value from the class file
+ * @return std::string: Name of the class
+ */
+std::string readNthLine(const std::string& filename, int n);
+
+}  // namespace proteus::util
+
+#endif  // GUARD_PROTEUS_UTIL_READ_NTH_LINE
diff --git a/src/proteus/helpers/string.hpp b/src/proteus/util/string.hpp
similarity index 94%
rename from src/proteus/helpers/string.hpp
rename to src/proteus/util/string.hpp
index 9f9aa1d48..f3f96f46f 100644
--- a/src/proteus/helpers/string.hpp
+++ b/src/proteus/util/string.hpp
@@ -4,7 +4,7 @@
 #include <algorithm>
 #include <string>
 
-namespace proteus {
+namespace proteus::util {
 
 /**
  * @brief Checks if a string ends with another string
@@ -30,6 +30,6 @@ inline void toLower(std::string* str) {
                  [](unsigned char c) { return std::tolower(c); });
 }
 
-}  // namespace proteus
+}  // namespace proteus::util
 
 #endif  // GUARD_PROTEUS_HELPERS_STRING
diff --git a/src/proteus/helpers/thread.hpp b/src/proteus/util/thread.hpp
similarity index 93%
rename from src/proteus/helpers/thread.hpp
rename to src/proteus/util/thread.hpp
index 66a9dc0e8..29986f7cc 100644
--- a/src/proteus/helpers/thread.hpp
+++ b/src/proteus/util/thread.hpp
@@ -21,7 +21,7 @@
 #include <sys/prctl.h>
 #endif
 
-namespace proteus {
+namespace proteus::util {
 /**
  * @brief Attempt to set the calling thread's name. Note, this may or may not
  * succeed. If renaming is not possible, it should silently fail without error.
@@ -40,9 +40,9 @@ inline void setThreadName(const char* name) {
 
 /// string overload for setThreadName
 inline void setThreadName(const std::string& name) {
-  setThreadName(name.c_str());
+  util::setThreadName(name.c_str());
 }
 
-}  // namespace proteus
+}  // namespace proteus::util
 
 #endif  // GUARD_PROTEUS_HELPERS_THREAD
diff --git a/src/proteus/util/traits.hpp b/src/proteus/util/traits.hpp
new file mode 100644
index 000000000..212cab581
--- /dev/null
+++ b/src/proteus/util/traits.hpp
@@ -0,0 +1,37 @@
+// Copyright 2022 Xilinx Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef GUARD_PROTEUS_UTIL_TRAITS
+#define GUARD_PROTEUS_UTIL_TRAITS
+
+#include <type_traits>
+
+namespace proteus::util {
+
+/**
+ * @brief Similar to std::is_same, returns true if T is any of the types in the
+ * variadic expression. Taken from https://stackoverflow.com/a/39550575/13621369
+ *
+ * @tparam T base type to check
+ * @tparam Ts list of types to compare against
+ */
+template <typename T, typename... Ts>
+struct is_any : std::disjunction<std::is_same<T, Ts>...> {};
+
+template <typename T, typename... Ts>
+inline constexpr bool is_any_v = is_any<T, Ts...>::value;
+
+}  // namespace proteus::util
+
+#endif  // GUARD_PROTEUS_UTIL_TRAITS
diff --git a/src/proteus/workers/CMakeLists.txt b/src/proteus/workers/CMakeLists.txt
index 3678692cd..e52cb7540 100644
--- a/src/proteus/workers/CMakeLists.txt
+++ b/src/proteus/workers/CMakeLists.txt
@@ -19,7 +19,6 @@ list(
          Echo
          InvertImage
          InvertVideo
-         # Fake
 )
 
 if(${PROTEUS_ENABLE_VITIS})
@@ -98,8 +97,6 @@ if(${PROTEUS_ENABLE_PTZENDNN})
   target_link_libraries(workerPtzendnn PRIVATE torch torch_cpu c10)
 endif()
 
-# target_link_libraries(workerFake PRIVATE observation)
-
 if(${PROTEUS_ENABLE_AKS})
 
   set(aks_workers Resnet50 Resnet50Stream AksDetect AksDetectStream)
diff --git a/src/proteus/workers/aks.cpp b/src/proteus/workers/aks.cpp
index e6a43dc29..78d909a95 100644
--- a/src/proteus/workers/aks.cpp
+++ b/src/proteus/workers/aks.cpp
@@ -40,12 +40,12 @@
 #include "proteus/core/data_types.hpp"        // for DataType, DataType::FP32
 #include "proteus/core/exceptions.hpp"        // for external_error
 #include "proteus/core/predict_api.hpp"       // for InferenceResponse, Infe...
-#include "proteus/helpers/declarations.hpp"   // for BufferPtrs, InferenceRe...
-#include "proteus/helpers/parse_env.hpp"      // for autoExpandEnvironmentVa...
-#include "proteus/helpers/thread.hpp"         // for setThreadName
+#include "proteus/declarations.hpp"           // for BufferPtrs, InferenceRe...
 #include "proteus/observation/logging.hpp"    // for Logger
 #include "proteus/observation/metrics.hpp"    // for Metrics, MetricSummaryIDs
 #include "proteus/observation/tracing.hpp"    // for Trace
+#include "proteus/util/parse_env.hpp"         // for autoExpandEnvironmentVa...
+#include "proteus/util/thread.hpp"            // for setThreadName
 #include "proteus/workers/worker.hpp"         // for Worker, kNumBufferAuto
 
 namespace AKS {
@@ -117,7 +117,7 @@ void Aks::doAcquire(RequestParameters* parameters) {
   if (parameters->has("aks_graph")) {
     path = parameters->get<std::string>("aks_graph");
   }
-  autoExpandEnvironmentVariables(path);
+  util::autoExpandEnvironmentVariables(path);
   this->sysMan_->loadGraphs(path);
 
   std::string graph_name = "graph_adder";
@@ -134,7 +134,7 @@ void Aks::doAcquire(RequestParameters* parameters) {
 }
 
 void Aks::doRun(BatchPtrQueue* input_queue) {
-  setThreadName("Aks");
+  util::setThreadName("Aks");
 #ifdef PROTEUS_ENABLE_LOGGING
   const auto& logger = this->getLogger();
 #endif
@@ -185,11 +185,10 @@ void Aks::doRun(BatchPtrQueue* input_queue) {
         output.setDatatype(DataType::FP32);
         output.setName("aks");
         output.setShape({1});
-        auto buffer = std::make_shared<std::vector<float>>();
-        buffer->resize(1);
-        (*buffer)[0] = value;
-        auto my_data_cast = std::reinterpret_pointer_cast<std::byte>(buffer);
-        output.setData(std::move(my_data_cast));
+        std::vector<std::byte> buffer;
+        buffer.resize(sizeof(float));
+        memcpy(buffer.data(), &value, sizeof(float));
+        output.setData(std::move(buffer));
         resp.addOutput(output);
       }
 
diff --git a/src/proteus/workers/aks_detect.cpp b/src/proteus/workers/aks_detect.cpp
index 8025ca07a..d06b48181 100644
--- a/src/proteus/workers/aks_detect.cpp
+++ b/src/proteus/workers/aks_detect.cpp
@@ -45,13 +45,13 @@
 #include "proteus/build_options.hpp"          // for PROTEUS_ENABLE_TRACING
 #include "proteus/core/data_types.hpp"        // for DataType, DataType::UINT32
 #include "proteus/core/predict_api.hpp"       // for InferenceResponse, Infe...
-#include "proteus/helpers/base64.hpp"         // for base64_decode
-#include "proteus/helpers/declarations.hpp"   // for BufferPtrs, InferenceRe...
-#include "proteus/helpers/parse_env.hpp"      // for autoExpandEnvironmentVa...
-#include "proteus/helpers/thread.hpp"         // for setThreadName
+#include "proteus/declarations.hpp"           // for BufferPtrs, InferenceRe...
 #include "proteus/observation/logging.hpp"    // for Logger
 #include "proteus/observation/metrics.hpp"    // for Metrics, MetricSummaryIDs
 #include "proteus/observation/tracing.hpp"    // for Trace
+#include "proteus/util/base64.hpp"            // for base64_decode
+#include "proteus/util/parse_env.hpp"         // for autoExpandEnvironmentVa...
+#include "proteus/util/thread.hpp"            // for setThreadName
 #include "proteus/workers/worker.hpp"         // for Worker, kNumBufferAuto
 
 namespace AKS {
@@ -129,6 +129,15 @@ size_t AksDetect::doAllocate(size_t num) {
   return buffer_num;
 }
 
+struct DetectResponse {
+  float class_id;
+  float score;
+  float x;
+  float y;
+  float w;
+  float h;
+};
+
 void AksDetect::doAcquire(RequestParameters* parameters) {
   auto kPath =
     std::string("${AKS_ROOT}/graph_zoo/graph_yolov3_u200_u250_proteus.json");
@@ -137,7 +146,7 @@ void AksDetect::doAcquire(RequestParameters* parameters) {
   if (parameters->has("aks_graph")) {
     path = parameters->get<std::string>("aks_graph");
   }
-  autoExpandEnvironmentVariables(path);
+  util::autoExpandEnvironmentVariables(path);
 
   this->sysMan_->loadGraphs(path);
 
@@ -152,7 +161,7 @@ void AksDetect::doAcquire(RequestParameters* parameters) {
 }
 
 void AksDetect::doRun(BatchPtrQueue* input_queue) {
-  setThreadName("AksDetect");
+  util::setThreadName("AksDetect");
 #ifdef PROTEUS_ENABLE_LOGGING
   const auto& logger = this->getLogger();
 #endif
@@ -208,7 +217,7 @@ void AksDetect::doRun(BatchPtrQueue* input_queue) {
                  input_buffer, input_size);
         } else if (input_dtype == DataType::STRING) {
           auto* idata = static_cast<char*>(input_buffer);
-          auto decoded_str = base64_decode(idata, input_size);
+          auto decoded_str = util::base64_decode(idata, input_size);
           std::vector<char> data(decoded_str.begin(), decoded_str.end());
           cv::Mat img = cv::imdecode(data, cv::IMREAD_UNCHANGED);
           if (img.empty()) {
@@ -249,22 +258,15 @@ void AksDetect::doRun(BatchPtrQueue* input_queue) {
     int size = shape.size() > 1 ? shape[0] * shape[1] : 0;
 
     auto* topKData = reinterpret_cast<float*>(outDD[0]->data().first);
-    auto my_data_2 = std::vector<std::shared_ptr<std::vector<float>>>();
-    my_data_2.reserve(this->batch_size_);
-    for (size_t i = 0; i < this->batch_size_; i++) {
-      my_data_2.push_back(std::make_shared<std::vector<float>>());
-    }
+    auto my_data_2 = std::vector<std::vector<std::byte>>();
+    my_data_2.resize(this->batch_size_);
     for (int i = 0; i < size; i += 7) {
       auto batch_id = static_cast<int>(topKData[i]);
-      auto len = my_data_2[batch_id]->size();
-      my_data_2[batch_id]->resize(len + 6);
-
-      (*my_data_2[batch_id].get())[len] = topKData[i + 1];      // class id
-      (*my_data_2[batch_id].get())[len + 1] = topKData[i + 2];  // score
-      (*my_data_2[batch_id].get())[len + 2] = topKData[i + 3];  // x
-      (*my_data_2[batch_id].get())[len + 3] = topKData[i + 4];  // y
-      (*my_data_2[batch_id].get())[len + 4] = topKData[i + 5];  // w
-      (*my_data_2[batch_id].get())[len + 5] = topKData[i + 6];  // h
+      auto len = my_data_2[batch_id].size();
+      my_data_2[batch_id].resize(len + sizeof(DetectResponse));
+
+      memcpy(my_data_2[batch_id].data() + len, &(topKData[i + 1]),
+             sizeof(DetectResponse));
     }
 
     tensor_count = 0;
@@ -287,11 +289,9 @@ void AksDetect::doRun(BatchPtrQueue* input_queue) {
           output.setName(output_name);
         }
 
-        output.setShape({6, my_data_2[tensor_count]->size() / 6});
-        auto my_data_cast =
-          std::reinterpret_pointer_cast<std::byte>(my_data_2[tensor_count]);
-        std::shared_ptr<std::byte> new_ptr(my_data_cast, my_data_cast.get());
-        output.setData(std::move(new_ptr));
+        output.setShape(
+          {6, my_data_2[tensor_count].size() / sizeof(DetectResponse)});
+        output.setData(std::move(my_data_2[tensor_count]));
         resp.addOutput(output);
         tensor_count++;
       }
diff --git a/src/proteus/workers/aks_detect_stream.cpp b/src/proteus/workers/aks_detect_stream.cpp
index 14d656afa..cae7b6f3a 100644
--- a/src/proteus/workers/aks_detect_stream.cpp
+++ b/src/proteus/workers/aks_detect_stream.cpp
@@ -44,12 +44,12 @@
 #include "proteus/build_options.hpp"          // for PROTEUS_ENABLE_TRACING
 #include "proteus/core/data_types.hpp"        // for DataType, DataType::STRING
 #include "proteus/core/predict_api.hpp"       // for InferenceResponse, Infe...
-#include "proteus/helpers/base64.hpp"         // for base64_encode
-#include "proteus/helpers/declarations.hpp"   // for BufferPtrs, InferenceRe...
-#include "proteus/helpers/parse_env.hpp"      // for autoExpandEnvironmentVa...
-#include "proteus/helpers/thread.hpp"         // for setThreadName
+#include "proteus/declarations.hpp"           // for BufferPtrs, InferenceRe...
 #include "proteus/observation/logging.hpp"    // for Logger
 #include "proteus/observation/tracing.hpp"    // for Trace
+#include "proteus/util/base64.hpp"            // for base64_encode
+#include "proteus/util/parse_env.hpp"         // for autoExpandEnvironmentVa...
+#include "proteus/util/thread.hpp"            // for setThreadName
 #include "proteus/workers/worker.hpp"         // for Worker, kNumBufferAuto
 
 namespace AKS {
@@ -129,7 +129,7 @@ void AksDetectStream::doAcquire(RequestParameters* parameters) {
   if (parameters->has("aks_graph")) {
     path = parameters->get<std::string>("aks_graph");
   }
-  autoExpandEnvironmentVariables(path);
+  util::autoExpandEnvironmentVariables(path);
   this->sysMan_->loadGraphs(path);
 
   auto graph_name = kGraphName;
@@ -147,7 +147,7 @@ void AksDetectStream::doAcquire(RequestParameters* parameters) {
 }
 
 void AksDetectStream::doRun(BatchPtrQueue* input_queue) {
-  setThreadName("AksDetectStream");
+  util::setThreadName("AksDetectStream");
 #ifdef PROTEUS_ENABLE_LOGGING
   const auto& logger = this->getLogger();
 #endif
@@ -172,10 +172,11 @@ void AksDetectStream::doRun(BatchPtrQueue* input_queue) {
       for (auto& input : inputs) {
         auto* input_buffer = input.getData();
 
-        auto* idata = static_cast<char*>(input_buffer);
+        const auto* idata = static_cast<char*>(input_buffer);
+        std::string data{idata, input.getSize()};
 
-        cv::VideoCapture cap(idata);  // open the video file
-        if (!cap.isOpened()) {        // check if we succeeded
+        cv::VideoCapture cap(data);  // open the video file
+        if (!cap.isOpened()) {       // check if we succeeded
           const char* error = "Cannot open video file";
           PROTEUS_LOG_ERROR(logger, error);
           req->runCallbackError(error);
@@ -205,7 +206,7 @@ void AksDetectStream::doRun(BatchPtrQueue* input_queue) {
         std::string metadata = "[" + std::to_string(video_width) + "," +
                                std::to_string(video_height) + "]";
         auto message = constructMessage(key, std::to_string(fps), metadata);
-        output.setData(&message);
+        output.setData(message.data());
         output.setShape({message.size()});
         resp.addOutput(output);
         req->runCallback(resp);
@@ -249,7 +250,7 @@ void AksDetectStream::doRun(BatchPtrQueue* input_queue) {
             std::vector<unsigned char> buf;
             cv::imencode(".jpg", frame, buf);
             const auto* enc_msg = reinterpret_cast<const char*>(buf.data());
-            std::string encoded = base64_encode(enc_msg, buf.size());
+            std::string encoded = util::base64_encode(enc_msg, buf.size());
             frames.push("data:image/jpg;base64," + encoded);
           }
           PROTEUS_LOG_INFO(logger, "Enqueuing in " + key);
@@ -295,7 +296,7 @@ void AksDetectStream::doRun(BatchPtrQueue* input_queue) {
               output.setName("image");
               output.setDatatype(DataType::STRING);
               auto message = constructMessage(key, frames.front(), labels[j]);
-              output.setData(&message);
+              output.setData(message.data());
               output.setShape({message.size()});
               resp.addOutput(output);
               req->runCallback(resp);
@@ -340,7 +341,7 @@ void AksDetectStream::doRun(BatchPtrQueue* input_queue) {
             output.setName("image");
             output.setDatatype(DataType::STRING);
             auto message = constructMessage(key, frames.front(), labels[j]);
-            output.setData(&message);
+            output.setData(message.data());
             output.setShape({message.size()});
             resp.addOutput(output);
             req->runCallback(resp);
diff --git a/src/proteus/workers/echo.cpp b/src/proteus/workers/echo.cpp
index d38ec9062..c09f1a66e 100644
--- a/src/proteus/workers/echo.cpp
+++ b/src/proteus/workers/echo.cpp
@@ -32,11 +32,11 @@
 #include "proteus/build_options.hpp"          // for PROTEUS_ENABLE_TRACING
 #include "proteus/core/data_types.hpp"        // for DataType, DataType::UINT32
 #include "proteus/core/predict_api.hpp"       // for InferenceRequest, Infere...
-#include "proteus/helpers/declarations.hpp"   // for BufferPtr, InferenceResp...
-#include "proteus/helpers/thread.hpp"         // for setThreadName
+#include "proteus/declarations.hpp"           // for BufferPtr, InferenceResp...
 #include "proteus/observation/logging.hpp"    // for Logger
 #include "proteus/observation/metrics.hpp"    // for Metrics
 #include "proteus/observation/tracing.hpp"    // for startFollowSpan, SpanPtr
+#include "proteus/util/thread.hpp"            // for setThreadName
 #include "proteus/workers/worker.hpp"         // for Worker
 
 namespace proteus {
@@ -112,7 +112,7 @@ void Echo::doAcquire(RequestParameters* parameters) {
 }
 
 void Echo::doRun(BatchPtrQueue* input_queue) {
-  setThreadName("Echo");
+  util::setThreadName("Echo");
 #ifdef PROTEUS_ENABLE_LOGGING
   const auto& logger = this->getLogger();
 #endif
@@ -168,11 +168,10 @@ void Echo::doRun(BatchPtrQueue* input_queue) {
           output.setName(output_name);
         }
         output.setShape({1});
-        auto buffer = std::make_shared<std::vector<uint32_t>>();
-        buffer->resize(1);
-        (*buffer)[0] = value;
-        auto my_data_cast = std::reinterpret_pointer_cast<std::byte>(buffer);
-        output.setData(std::move(my_data_cast));
+        std::vector<std::byte> buffer;
+        buffer.resize(sizeof(uint32_t));
+        memcpy(buffer.data(), &value, sizeof(uint32_t));
+        output.setData(std::move(buffer));
         resp.addOutput(output);
       }
 
diff --git a/src/proteus/workers/invert_image.cpp b/src/proteus/workers/invert_image.cpp
index 0746f7657..eaed4b37a 100644
--- a/src/proteus/workers/invert_image.cpp
+++ b/src/proteus/workers/invert_image.cpp
@@ -38,12 +38,12 @@
 #include "proteus/build_options.hpp"          // for PROTEUS_ENABLE_TRACING
 #include "proteus/core/data_types.hpp"        // for DataType, DataType::UINT8
 #include "proteus/core/predict_api.hpp"       // for InferenceRequest, Infere...
-#include "proteus/helpers/base64.hpp"         // for base64_decode, base64_en...
-#include "proteus/helpers/declarations.hpp"   // for BufferPtr, InferenceResp...
-#include "proteus/helpers/thread.hpp"         // for setThreadName
+#include "proteus/declarations.hpp"           // for BufferPtr, InferenceResp...
 #include "proteus/observation/logging.hpp"    // for Logger
 #include "proteus/observation/metrics.hpp"    // for Metrics
 #include "proteus/observation/tracing.hpp"    // for startFollowSpan, SpanPtr
+#include "proteus/util/base64.hpp"            // for base64_decode, base64_en...
+#include "proteus/util/thread.hpp"            // for setThreadName
 #include "proteus/workers/worker.hpp"         // for Worker
 
 namespace {
@@ -144,7 +144,7 @@ void InvertImage::doAcquire(RequestParameters* parameters) {
 }
 
 void InvertImage::doRun(BatchPtrQueue* input_queue) {
-  setThreadName("InvertImage");
+  util::setThreadName("InvertImage");
 #ifdef PROTEUS_ENABLE_LOGGING
   const auto& logger = this->getLogger();
 #endif
@@ -191,15 +191,14 @@ void InvertImage::doRun(BatchPtrQueue* input_queue) {
 
           auto* output_data = static_cast<uint8_t*>(output_buffer);
 
-          auto buffer = std::make_shared<std::vector<uint8_t>>();
-          buffer->resize(input_size);
-          memcpy(buffer->data(), output_data, input_size);
-          auto my_data_cast = std::reinterpret_pointer_cast<std::byte>(buffer);
-          output.setData(std::move(my_data_cast));
+          std::vector<std::byte> buffer;
+          buffer.resize(input_size);
+          memcpy(buffer.data(), output_data, input_size);
+          output.setData(std::move(buffer));
           output.setDatatype(DataType::UINT8);
         } else if (input_dtype == DataType::STRING) {
           auto* idata = static_cast<char*>(input_buffer);
-          auto decoded_str = base64_decode(idata, input_size);
+          auto decoded_str = util::base64_decode(idata, input_size);
           std::vector<char> data(decoded_str.begin(), decoded_str.end());
           cv::Mat img;
           try {
@@ -220,12 +219,13 @@ void InvertImage::doRun(BatchPtrQueue* input_queue) {
           std::vector<unsigned char> buf;
           cv::imencode(".jpg", img, buf);
           const auto* enc_msg = reinterpret_cast<const char*>(buf.data());
-          auto encoded =
-            std::make_shared<std::string>(base64_encode(enc_msg, buf.size()));
-          auto my_data_cast = std::reinterpret_pointer_cast<std::byte>(encoded);
-          output.setData(std::move(my_data_cast));
+          auto encoded = util::base64_encode(enc_msg, buf.size());
+          std::vector<std::byte> buffer;
+          buffer.resize(encoded.size());
+          memcpy(buffer.data(), encoded.data(), encoded.length());
+          output.setData(std::move(buffer));
           output.setDatatype(DataType::STRING);
-          output.setShape({buf.size()});
+          output.setShape({encoded.size()});
         }
 
         // if our output is explicitly named, use that name in response, or use
diff --git a/src/proteus/workers/invert_video.cpp b/src/proteus/workers/invert_video.cpp
index 019eba130..6c8ad5c7b 100644
--- a/src/proteus/workers/invert_video.cpp
+++ b/src/proteus/workers/invert_video.cpp
@@ -32,11 +32,11 @@
 #include "proteus/build_options.hpp"          // for PROTEUS_ENABLE_TRACING
 #include "proteus/core/data_types.hpp"        // for DataType, DataType::STRING
 #include "proteus/core/predict_api.hpp"       // for InferenceResponse, Infer...
-#include "proteus/helpers/base64.hpp"         // for base64_encode
-#include "proteus/helpers/declarations.hpp"   // for BufferPtr, InferenceResp...
-#include "proteus/helpers/thread.hpp"         // for setThreadName
+#include "proteus/declarations.hpp"           // for BufferPtr, InferenceResp...
 #include "proteus/observation/logging.hpp"    // for Logger
 #include "proteus/observation/tracing.hpp"    // for startFollowSpan, SpanPtr
+#include "proteus/util/base64.hpp"            // for base64_encode
+#include "proteus/util/thread.hpp"            // for setThreadName
 #include "proteus/workers/worker.hpp"         // for Worker
 
 namespace proteus {
@@ -107,7 +107,7 @@ void InvertVideo::doAcquire(RequestParameters* parameters) {
 }
 
 void InvertVideo::doRun(BatchPtrQueue* input_queue) {
-  setThreadName("InvertVideo");
+  util::setThreadName("InvertVideo");
 #ifdef PROTEUS_ENABLE_LOGGING
   const auto& logger = this->getLogger();
 #endif
@@ -158,7 +158,7 @@ void InvertVideo::doRun(BatchPtrQueue* input_queue) {
         output.setName("key");
         output.setDatatype(DataType::STRING);
         auto message = constructMessage(key, std::to_string(fps));
-        output.setData(&message);
+        output.setData(message.data());
         output.setShape({message.size()});
         resp.addOutput(output);
         req->runCallback(resp);
@@ -174,7 +174,7 @@ void InvertVideo::doRun(BatchPtrQueue* input_queue) {
           cv::imencode(".jpg", frame, buf);
           const auto* enc_msg = reinterpret_cast<const char*>(buf.data());
           std::string encoded =
-            "data:image/jpg;base64," + base64_encode(enc_msg, buf.size());
+            "data:image/jpg;base64," + util::base64_encode(enc_msg, buf.size());
 
           InferenceResponse resp;
           resp.setID(req->getID());
@@ -183,8 +183,8 @@ void InvertVideo::doRun(BatchPtrQueue* input_queue) {
           InferenceResponseOutput output;
           output.setName("image");
           output.setDatatype(DataType::STRING);
-          auto message = constructMessage(key, encoded);
-          output.setData(&message);
+          message = constructMessage(key, encoded);
+          output.setData(message.data());
           output.setShape({message.size()});
           resp.addOutput(output);
           req->runCallback(resp);
diff --git a/src/proteus/workers/migraphx.cpp b/src/proteus/workers/migraphx.cpp
index 49ed041b9..beb4cf9b5 100644
--- a/src/proteus/workers/migraphx.cpp
+++ b/src/proteus/workers/migraphx.cpp
@@ -34,11 +34,11 @@
 #include "proteus/build_options.hpp"          // for PROTEUS_ENABLE_TRACING
 #include "proteus/core/data_types.hpp"        // for DataType, DataType::UINT32
 #include "proteus/core/predict_api.hpp"       // for InferenceRequest, Infere...
-#include "proteus/helpers/declarations.hpp"   // for BufferPtr, InferenceResp...
-#include "proteus/helpers/thread.hpp"         // for setThreadName
+#include "proteus/declarations.hpp"           // for BufferPtr, InferenceResp...
 #include "proteus/observation/logging.hpp"    // for SPDLOG_LOGGER_INFO, SPDL...
 #include "proteus/observation/metrics.hpp"    // for Metrics
 #include "proteus/observation/tracing.hpp"    // for startFollowSpan, SpanPtr
+#include "proteus/util/thread.hpp"            // for setThreadName
 #include "proteus/workers/worker.hpp"         // for Worker
 
 // opencv for debugging only --
@@ -362,7 +362,7 @@ void MIGraphXWorker::doRun(BatchPtrQueue* input_queue) {
   PROTEUS_LOG_INFO(logger, "beginning of MIGraphXWorker::doRun");
 
   // std::shared_ptr<InferenceRequest> req;
-  setThreadName("Migraphx");
+  util::setThreadName("Migraphx");
 
   //
   //  Wait for requests from the batcher in an infinite loop.  This thread will
@@ -540,12 +540,10 @@ void MIGraphXWorker::doRun(BatchPtrQueue* input_queue) {
             output.setData(results);
 
             // Copy migraphx results to a buffer and add to output
-            auto buffer = std::make_shared<std::vector<std::byte>>();
-            buffer->resize(size_of_result);
-            memcpy(&((*buffer)[0]), results, size_of_result);
-            auto my_data_cast =
-              std::reinterpret_pointer_cast<std::byte>(buffer);
-            output.setData(std::move(my_data_cast));
+            std::vector<std::byte> buffer;
+            buffer.resize(size_of_result);
+            memcpy(buffer.data(), results, size_of_result);
+            output.setData(std::move(buffer));
             resp.addOutput(output);
           }
 
diff --git a/src/proteus/workers/pt_zendnn.cpp b/src/proteus/workers/pt_zendnn.cpp
index 2930d66d2..2df75e850 100644
--- a/src/proteus/workers/pt_zendnn.cpp
+++ b/src/proteus/workers/pt_zendnn.cpp
@@ -41,11 +41,11 @@
 #include "proteus/core/data_types.hpp"        // for DataType, DataType::FP32
 #include "proteus/core/exceptions.hpp"        // for external_error, file_no...
 #include "proteus/core/predict_api.hpp"       // for InferenceResponse, Requ...
-#include "proteus/helpers/declarations.hpp"   // for InferenceResponseOutput
-#include "proteus/helpers/thread.hpp"         // for setThreadName
+#include "proteus/declarations.hpp"           // for InferenceResponseOutput
 #include "proteus/observation/logging.hpp"    // for Logger, PROTEUS_LOG_INFO
 #include "proteus/observation/metrics.hpp"    // for Metrics, MetricCounterIDs
 #include "proteus/observation/tracing.hpp"    // for Trace
+#include "proteus/util/thread.hpp"            // for setThreadName
 #include "proteus/workers/worker.hpp"         // for Worker, kNumBufferAuto
 
 namespace fs = std::filesystem;
@@ -201,7 +201,7 @@ void PtZendnn::doAcquire(RequestParameters* parameters) {
 
 void PtZendnn::doRun(BatchPtrQueue* input_queue) {
   std::unique_ptr<Batch> batch;
-  setThreadName("PtZendnn");
+  util::setThreadName("PtZendnn");
 #ifdef PROTEUS_ENABLE_LOGGING
   const auto& logger = this->getLogger();
 #endif
@@ -306,13 +306,12 @@ void PtZendnn::doRun(BatchPtrQueue* input_queue) {
 
       for (unsigned int i = 0; i < inputs.size(); i++) {
         InferenceResponseOutput output;
-        auto buffer = std::make_shared<std::vector<float>>();
-        buffer->resize(response_size);
+        std::vector<std::byte> buffer;
+        buffer.resize(response_size * sizeof(float));
 
-        memcpy(buffer->data(), output_tensor[i].data_ptr<float>(),
+        memcpy(buffer.data(), output_tensor[i].data_ptr<float>(),
                response_size * sizeof(float));
-        auto my_data_cast = std::reinterpret_pointer_cast<std::byte>(buffer);
-        output.setData(my_data_cast);
+        output.setData(std::move(buffer));
 
         std::string output_name = outputs[i].getName();
         if (output_name.empty()) {
diff --git a/src/proteus/workers/resnet50.cpp b/src/proteus/workers/resnet50.cpp
index 94d608f88..e08a75ebc 100644
--- a/src/proteus/workers/resnet50.cpp
+++ b/src/proteus/workers/resnet50.cpp
@@ -44,13 +44,13 @@
 #include "proteus/build_options.hpp"          // for PROTEUS_ENABLE_TRACING
 #include "proteus/core/data_types.hpp"        // for DataType, DataType::UINT32
 #include "proteus/core/predict_api.hpp"       // for InferenceResponse, Infe...
-#include "proteus/helpers/base64.hpp"         // for base64_decode
-#include "proteus/helpers/declarations.hpp"   // for BufferPtrs, InferenceRe...
-#include "proteus/helpers/parse_env.hpp"      // for autoExpandEnvironmentVa...
-#include "proteus/helpers/thread.hpp"         // for setThreadName
+#include "proteus/declarations.hpp"           // for BufferPtrs, InferenceRe...
 #include "proteus/observation/logging.hpp"    // for Logger
 #include "proteus/observation/metrics.hpp"    // for Metrics, MetricSummaryIDs
 #include "proteus/observation/tracing.hpp"    // for Trace
+#include "proteus/util/base64.hpp"            // for base64_decode
+#include "proteus/util/parse_env.hpp"         // for autoExpandEnvironmentVa...
+#include "proteus/util/thread.hpp"            // for setThreadName
 #include "proteus/workers/worker.hpp"         // for Worker, kNumBufferAuto
 
 namespace AKS {
@@ -136,7 +136,7 @@ void ResNet50::doAcquire(RequestParameters* parameters) {
   if (parameters->has("aks_graph")) {
     path = parameters->get<std::string>("aks_graph");
   }
-  autoExpandEnvironmentVariables(path);
+  util::autoExpandEnvironmentVariables(path);
   this->sysMan_->loadGraphs(path);
 
   this->graph_ = this->sysMan_->getGraph(this->graphName_);
@@ -151,7 +151,7 @@ void ResNet50::doAcquire(RequestParameters* parameters) {
 
 void ResNet50::doRun(BatchPtrQueue* input_queue) {
   std::shared_ptr<InferenceRequest> req;
-  setThreadName("ResNet50");
+  util::setThreadName("ResNet50");
 #ifdef PROTEUS_ENABLE_LOGGING
   const auto& logger = this->getLogger();
 #endif
@@ -206,7 +206,7 @@ void ResNet50::doRun(BatchPtrQueue* input_queue) {
                  input_buffer, input_size);
         } else if (input_dtype == DataType::STRING) {
           auto* idata = static_cast<char*>(input_buffer);
-          auto decoded_str = base64_decode(idata, input_size);
+          auto decoded_str = util::base64_decode(idata, input_size);
           std::vector<char> data(decoded_str.begin(), decoded_str.end());
           cv::Mat img = cv::imdecode(data, cv::IMREAD_UNCHANGED);
           if (img.empty()) {
@@ -268,12 +268,11 @@ void ResNet50::doRun(BatchPtrQueue* input_queue) {
 
       for (unsigned int i = 0; i < inputs.size(); i++) {
         InferenceResponseOutput output;
-        auto buffer = std::make_shared<std::vector<int>>();
-        buffer->resize(response_size);
-        memcpy(buffer->data(), topKData + (i * response_size),
+        std::vector<std::byte> buffer;
+        buffer.resize(response_size * sizeof(int));
+        memcpy(buffer.data(), topKData + (i * response_size),
                response_size * sizeof(int));
-        auto my_data_cast = std::reinterpret_pointer_cast<std::byte>(buffer);
-        output.setData(my_data_cast);
+        output.setData(std::move(buffer));
 
         std::string output_name = outputs[i].getName();
         if (output_name.empty()) {
diff --git a/src/proteus/workers/resnet50_stream.cpp b/src/proteus/workers/resnet50_stream.cpp
index 06f8c87f1..e72a53418 100644
--- a/src/proteus/workers/resnet50_stream.cpp
+++ b/src/proteus/workers/resnet50_stream.cpp
@@ -45,11 +45,11 @@
 #include "proteus/build_options.hpp"          // for PROTEUS_ENABLE_LOGGING
 #include "proteus/core/data_types.hpp"        // for DataType, DataType::STRING
 #include "proteus/core/predict_api.hpp"       // for InferenceResponse, Infe...
-#include "proteus/helpers/base64.hpp"         // for base64_encode
-#include "proteus/helpers/declarations.hpp"   // for BufferPtrs, InferenceRe...
-#include "proteus/helpers/parse_env.hpp"      // for autoExpandEnvironmentVa...
-#include "proteus/helpers/thread.hpp"         // for setThreadName
+#include "proteus/declarations.hpp"           // for BufferPtrs, InferenceRe...
 #include "proteus/observation/logging.hpp"    // for Logger
+#include "proteus/util/base64.hpp"            // for base64_encode
+#include "proteus/util/parse_env.hpp"         // for autoExpandEnvironmentVa...
+#include "proteus/util/thread.hpp"            // for setThreadName
 #include "proteus/workers/worker.hpp"         // for Worker, kNumBufferAuto
 
 namespace AKS {
@@ -139,7 +139,7 @@ void ResNet50Stream::doAcquire(RequestParameters* parameters) {
   if (parameters->has("aks_graph")) {
     path = parameters->get<std::string>("aks_graph");
   }
-  autoExpandEnvironmentVariables(path);
+  util::autoExpandEnvironmentVariables(path);
   this->sysMan_->loadGraphs(path);
 
   this->graph_ = this->sysMan_->getGraph(this->graphName_);
@@ -154,7 +154,7 @@ void ResNet50Stream::doAcquire(RequestParameters* parameters) {
 
 void ResNet50Stream::doRun(BatchPtrQueue* input_queue) {
   std::shared_ptr<InferenceRequest> req;
-  setThreadName("ResNet50Stream");
+  util::setThreadName("ResNet50Stream");
 #ifdef PROTEUS_ENABLE_LOGGING
   const auto& logger = this->getLogger();
 #endif
@@ -174,10 +174,11 @@ void ResNet50Stream::doRun(BatchPtrQueue* input_queue) {
       for (auto& input : inputs) {
         auto* input_buffer = input.getData();
 
-        auto* idata = static_cast<char*>(input_buffer);
+        const auto* idata = static_cast<char*>(input_buffer);
+        std::string data{idata, input.getSize()};
 
-        cv::VideoCapture cap(idata);  // open the video file
-        if (!cap.isOpened()) {        // check if we succeeded
+        cv::VideoCapture cap(data);  // open the video file
+        if (!cap.isOpened()) {       // check if we succeeded
           const char* error = "Cannot open video file";
           PROTEUS_LOG_ERROR(logger, error);
           req->runCallbackError(error);
@@ -205,7 +206,7 @@ void ResNet50Stream::doRun(BatchPtrQueue* input_queue) {
         std::string metadata = "[" + std::to_string(video_width) + "," +
                                std::to_string(video_height) + "]";
         auto message = constructMessage(key, std::to_string(fps), metadata);
-        output.setData(&message);
+        output.setData(message.data());
         output.setShape({message.size()});
         resp.addOutput(output);
         req->runCallback(resp);
@@ -240,7 +241,7 @@ void ResNet50Stream::doRun(BatchPtrQueue* input_queue) {
             std::vector<unsigned char> buf;
             cv::imencode(".jpg", frame, buf);
             const auto* enc_msg = reinterpret_cast<const char*>(buf.data());
-            std::string encoded = base64_encode(enc_msg, buf.size());
+            std::string encoded = util::base64_encode(enc_msg, buf.size());
             frames.push("data:image/jpg;base64," + encoded);
           }
           futures.push(
@@ -271,7 +272,7 @@ void ResNet50Stream::doRun(BatchPtrQueue* input_queue) {
               output.setName("image");
               output.setDatatype(DataType::STRING);
               auto message = constructMessage(key, frames.front(), labels);
-              output.setData(&message);
+              output.setData(message.data());
               output.setShape({message.size()});
               resp.addOutput(output);
               req->runCallback(resp);
@@ -304,7 +305,7 @@ void ResNet50Stream::doRun(BatchPtrQueue* input_queue) {
             output.setName("image");
             output.setDatatype(DataType::STRING);
             auto message = constructMessage(key, frames.front(), labels);
-            output.setData(&message);
+            output.setData(message.data());
             output.setShape({message.size()});
             resp.addOutput(output);
             req->runCallback(resp);
diff --git a/src/proteus/workers/tf_zendnn.cpp b/src/proteus/workers/tf_zendnn.cpp
index 141c17653..fecd1bb66 100644
--- a/src/proteus/workers/tf_zendnn.cpp
+++ b/src/proteus/workers/tf_zendnn.cpp
@@ -49,12 +49,12 @@
 #include "proteus/core/data_types.hpp"        // for DataType, Dat...
 #include "proteus/core/exceptions.hpp"        // for external_error
 #include "proteus/core/predict_api.hpp"       // for InferenceResp...
-#include "proteus/helpers/declarations.hpp"   // for InferenceResp...
-#include "proteus/helpers/string.hpp"         // for endsWith
-#include "proteus/helpers/thread.hpp"         // for setThreadName
+#include "proteus/declarations.hpp"           // for InferenceResp...
 #include "proteus/observation/logging.hpp"    // for Logger, PROTE...
 #include "proteus/observation/metrics.hpp"    // for Metrics, Metr...
 #include "proteus/observation/tracing.hpp"    // for Trace
+#include "proteus/util/string.hpp"            // for endsWith
+#include "proteus/util/thread.hpp"            // for setThreadName
 #include "proteus/workers/worker.hpp"         // for Worker, kNumB...
 
 namespace tf = ::tensorflow;
@@ -203,7 +203,7 @@ void TfZendnn::doAcquire(RequestParameters* parameters) {
   std::string path;
   if (parameters->has("model")) {
     path = parameters->get<std::string>("model");
-    if (!endsWith(path, ".pb")) {
+    if (!util::endsWith(path, ".pb")) {
       path += ".pb";
     }
   } else {
@@ -232,7 +232,7 @@ void TfZendnn::doAcquire(RequestParameters* parameters) {
 }
 
 void TfZendnn::doRun(BatchPtrQueue* input_queue) {
-  setThreadName("TfZendnn");
+  util::setThreadName("TfZendnn");
 #ifdef PROTEUS_ENABLE_LOGGING
   const auto& logger = this->getLogger();
 #endif
@@ -332,14 +332,13 @@ void TfZendnn::doRun(BatchPtrQueue* input_queue) {
 
       for (unsigned int i = 0; i < inputs.size(); i++) {
         InferenceResponseOutput output;
-        auto buffer = std::make_shared<std::vector<float>>();
-        buffer->resize(response_size);
+        std::vector<std::byte> buffer;
+        buffer.resize(response_size * sizeof(float));
 
-        memcpy(buffer->data(),
+        memcpy(buffer.data(),
                output_tensor[0].flat<float>().data() + (i * response_size),
                response_size * sizeof(float));
-        auto my_data_cast = std::reinterpret_pointer_cast<std::byte>(buffer);
-        output.setData(my_data_cast);
+        output.setData(std::move(buffer));
 
         std::string output_name = outputs[i].getName();
         if (output_name.empty()) {
diff --git a/src/proteus/workers/xmodel.cpp b/src/proteus/workers/xmodel.cpp
index b2ae97c1b..3e09790df 100644
--- a/src/proteus/workers/xmodel.cpp
+++ b/src/proteus/workers/xmodel.cpp
@@ -49,15 +49,15 @@
 #include "proteus/build_options.hpp"               // for PROTEUS_ENABLE_MET...
 #include "proteus/core/data_types.hpp"             // for mapXirType, DataType
 #include "proteus/core/predict_api.hpp"            // for InferenceResponse
-#include "proteus/helpers/ctpl.h"                  // for thread_pool
-#include "proteus/helpers/declarations.hpp"        // for BufferPtrs, Infere...
-#include "proteus/helpers/parse_env.hpp"           // for autoExpandEnvironm...
-#include "proteus/helpers/queue.hpp"               // for BufferPtrsQueue
-#include "proteus/helpers/string.hpp"              // for endsWith
-#include "proteus/helpers/thread.hpp"              // for setThreadName
+#include "proteus/declarations.hpp"                // for BufferPtrs, Infere...
 #include "proteus/observation/logging.hpp"         // for Logger
 #include "proteus/observation/metrics.hpp"         // for Metrics, MetricCou...
 #include "proteus/observation/tracing.hpp"         // for Trace
+#include "proteus/util/ctpl.h"                     // for thread_pool
+#include "proteus/util/parse_env.hpp"              // for autoExpandEnvironm...
+#include "proteus/util/queue.hpp"                  // for BufferPtrsQueue
+#include "proteus/util/string.hpp"                 // for endsWith
+#include "proteus/util/thread.hpp"                 // for setThreadName
 #include "proteus/workers/worker.hpp"              // for Worker, kNumBuffer...
 
 uint64_t reduce_mult(std::vector<uint64_t>& v) {
@@ -130,11 +130,11 @@ void XModel::doInit(RequestParameters* parameters) {
   auto path = kPath;
   if (parameters->has("model")) {
     path = parameters->get<std::string>("model");
-    if (!endsWith(path, ".xmodel")) {
+    if (!util::endsWith(path, ".xmodel")) {
       path += ".xmodel";
     }
   }
-  autoExpandEnvironmentVariables(path);
+  util::autoExpandEnvironmentVariables(path);
   graph_ = xir::Graph::deserialize(path);
 
   std::vector<xir::Subgraph*> subgraphs =
@@ -224,7 +224,7 @@ void XModel::doAcquire(RequestParameters* parameters) {
 void XModel::doRun(BatchPtrQueue* input_queue) {
   std::atomic_int32_t pool_size = 0;
   const int max_pool_size = this->pool_.size() * 4;  // 4 is arbitrary
-  setThreadName("XModel");
+  util::setThreadName("XModel");
 #ifdef PROTEUS_ENABLE_LOGGING
   const auto& logger = this->getLogger();
 #endif
@@ -331,14 +331,13 @@ void XModel::doRun(BatchPtrQueue* input_queue) {
 
           output.setDatatype(this->output_type_);
 
-          auto buffer = std::make_shared<std::vector<int8_t>>();
-          buffer->resize(this->output_size_);
-          memcpy(buffer->data(),
+          std::vector<std::byte> buffer;
+          buffer.resize(this->output_size_ * sizeof(uint8_t));
+          memcpy(buffer.data(),
                  reinterpret_cast<int8_t*>(output_index) +
                    (tensor_count * this->output_size_),
-                 this->output_size_);
-          auto my_data_cast = std::reinterpret_pointer_cast<std::byte>(buffer);
-          output.setData(my_data_cast);
+                 this->output_size_ * sizeof(uint8_t));
+          output.setData(std::move(buffer));
 
           std::string output_name = outputs[i].getName();
 


Test patch:
diff --git a/tests/api/test_model_infer.cpp b/tests/api/test_model_infer.cpp
index ac0af0143..a6444ab00 100644
--- a/tests/api/test_model_infer.cpp
+++ b/tests/api/test_model_infer.cpp
@@ -42,9 +42,8 @@ void test(proteus::Client* client) {
   auto outputs = response.getOutputs();
   EXPECT_EQ(outputs.size(), 1);
   for (auto& output : outputs) {
-    auto* data = static_cast<std::vector<uint32_t>*>(output.getData());
-    EXPECT_EQ(data->size(), 1);
-    EXPECT_EQ((*data)[0], 2);
+    const auto* data = static_cast<uint32_t*>(output.getData());
+    EXPECT_EQ(data[0], 2);
   }
 
   client->modelUnload(endpoint);
diff --git a/tests/api/test_model_infer_async.cpp b/tests/api/test_model_infer_async.cpp
index 33e4d0326..93b3bdee0 100644
--- a/tests/api/test_model_infer_async.cpp
+++ b/tests/api/test_model_infer_async.cpp
@@ -52,9 +52,8 @@ void test(proteus::Client* client) {
     auto outputs = response.getOutputs();
     EXPECT_EQ(outputs.size(), 1);
     for (const auto& output : outputs) {
-      auto* data = static_cast<std::vector<uint32_t>*>(output.getData());
-      EXPECT_EQ(data->size(), 1);
-      EXPECT_EQ((*data)[0], 2);
+      const auto* data = static_cast<uint32_t*>(output.getData());
+      EXPECT_EQ(data[0], 2);
     }
   }
 
diff --git a/tests/models/test_mnist.cpp b/tests/models/test_mnist.cpp
index 1ab393ffa..adbbf87c0 100644
--- a/tests/models/test_mnist.cpp
+++ b/tests/models/test_mnist.cpp
@@ -44,7 +44,7 @@ void test(proteus::Client* client) {
   input_0.setName("input0");
   input_0.setDatatype(DataType::FP32);
   input_0.setShape({28, 28, 1});
-  input_0.setData((void*)(img.data));
+  input_0.setData(img.data);
 
   InferenceRequest request;
   request.addInputTensor(input_0);
@@ -56,8 +56,7 @@ void test(proteus::Client* client) {
   const auto& output = outputs[0];
   EXPECT_EQ(output.getSize(), 10);
   EXPECT_EQ(output.getDatatype(), DataType::FP32);
-  const auto data = *static_cast<std::vector<float>*>(output.getData());
-  EXPECT_EQ(data.size(), 10);
+  const auto* data = static_cast<float*>(output.getData());
 
   float max = 0;
   int index = -1;
diff --git a/tests/performance/batching/perf_soft.cpp b/tests/performance/batching/perf_soft.cpp
index f9c42802f..ba1a6f31a 100644
--- a/tests/performance/batching/perf_soft.cpp
+++ b/tests/performance/batching/perf_soft.cpp
@@ -32,7 +32,7 @@
 #include "proteus/core/data_types.hpp"          // for DataType, DataType::U...
 #include "proteus/core/predict_api.hpp"         // for InferenceRequest, Req...
 #include "proteus/core/worker_info.hpp"         // for WorkerInfo
-#include "proteus/helpers/declarations.hpp"     // for BufferPtrs
+#include "proteus/declarations.hpp"             // for BufferPtrs
 
 namespace proteus {
 
diff --git a/tests/src/proteus/CMakeLists.txt b/tests/src/proteus/CMakeLists.txt
index 84678d5c0..a3d6ec7f4 100644
--- a/tests/src/proteus/CMakeLists.txt
+++ b/tests/src/proteus/CMakeLists.txt
@@ -15,6 +15,5 @@
 add_subdirectory(batching)
 # add_subdirectory(buffers) add_subdirectory(clients)
 add_subdirectory(core)
-# add_subdirectory(helpers)
 add_subdirectory(observation)
 # add_subdirectory(servers) add_subdirectory(testing) add_subdirectory(workers)
diff --git a/tests/src/proteus/batching/batcher.cpp b/tests/src/proteus/batching/batcher.cpp
index 1fcef864a..1c6f1e861 100644
--- a/tests/src/proteus/batching/batcher.cpp
+++ b/tests/src/proteus/batching/batcher.cpp
@@ -32,10 +32,10 @@
 #include "proteus/core/fake_predict_api.hpp"  // for FakeInferenceRequest
 #include "proteus/core/interface.hpp"         // for Interface
 #include "proteus/core/predict_api.hpp"       // for InferenceResponsePromis...
-#include "proteus/helpers/queue.hpp"          // for BlockingConcurrentQueue
-#include "proteus/helpers/thread.hpp"         // for setThreadName
 #include "proteus/observation/logging.hpp"    // for Logger, PROTEUS_LOG_DEBUG
 #include "proteus/observation/tracing.hpp"    // for Trace
+#include "proteus/util/queue.hpp"             // for BlockingConcurrentQueue
+#include "proteus/util/thread.hpp"            // for setThreadName
 
 // IWYU pragma: no_forward_declare proteus::Buffer
 
@@ -99,7 +99,7 @@ void FakeInterface::errorHandler(const std::exception &e) {
 
 void Batcher::run(WorkerInfo *worker) {
   auto thread_name = "batch" + this->getName();
-  setThreadName(thread_name);
+  util::setThreadName(thread_name);
   InterfacePtr req;
   bool run = true;
 
diff --git a/tests/src/proteus/core/fake_predict_api.hpp b/tests/src/proteus/core/fake_predict_api.hpp
index 94cad0eeb..7192d72a8 100644
--- a/tests/src/proteus/core/fake_predict_api.hpp
+++ b/tests/src/proteus/core/fake_predict_api.hpp
@@ -23,8 +23,8 @@
 #include <cstddef>  // for size_t
 #include <vector>   // for vector
 
-#include "proteus/core/predict_api.hpp"      // for InferenceRequest, Infere...
-#include "proteus/helpers/declarations.hpp"  // for BufferRawPtrs
+#include "proteus/core/predict_api.hpp"  // for InferenceRequest, Infere...
+#include "proteus/declarations.hpp"      // for BufferRawPtrs
 
 namespace proteus {
 
diff --git a/tests/src/proteus/core/predict_api.cpp b/tests/src/proteus/core/predict_api.cpp
index c63ac8150..87513428b 100644
--- a/tests/src/proteus/core/predict_api.cpp
+++ b/tests/src/proteus/core/predict_api.cpp
@@ -25,7 +25,7 @@
 #include <vector>   // for vector
 
 #include "proteus/core/fake_predict_api.hpp"  // for FakeInferenceRequest
-#include "proteus/helpers/declarations.hpp"   // for BufferRawPtrs
+#include "proteus/declarations.hpp"           // for BufferRawPtrs
 
 namespace proteus {
 
diff --git a/tests/src/proteus/core/worker_info_buffers_finite.cpp b/tests/src/proteus/core/worker_info_buffers_finite.cpp
index ad25de4ad..2f884935e 100644
--- a/tests/src/proteus/core/worker_info_buffers_finite.cpp
+++ b/tests/src/proteus/core/worker_info_buffers_finite.cpp
@@ -25,12 +25,12 @@
 #include <thread>   // for thread::id, thread
 #include <utility>  // for move
 
-#include "proteus/batching/batcher.hpp"      // for Batcher
-#include "proteus/buffers/buffer.hpp"        // IWYU pragma: keep
-#include "proteus/core/predict_api.hpp"      // for RequestParameters
-#include "proteus/core/worker_info.hpp"      // for WorkerInfo
-#include "proteus/helpers/declarations.hpp"  // for BufferPtrs
-#include "proteus/helpers/queue.hpp"         // for BufferPtrsQueue, Blockin...
+#include "proteus/batching/batcher.hpp"  // for Batcher
+#include "proteus/buffers/buffer.hpp"    // IWYU pragma: keep
+#include "proteus/core/predict_api.hpp"  // for RequestParameters
+#include "proteus/core/worker_info.hpp"  // for WorkerInfo
+#include "proteus/declarations.hpp"      // for BufferPtrs
+#include "proteus/util/queue.hpp"        // for BufferPtrsQueue, Blockin...
 
 namespace proteus {
 
diff --git a/tests/src/proteus/core/worker_info_buffers_infinite.cpp b/tests/src/proteus/core/worker_info_buffers_infinite.cpp
index a7db1220c..ec49319bb 100644
--- a/tests/src/proteus/core/worker_info_buffers_infinite.cpp
+++ b/tests/src/proteus/core/worker_info_buffers_infinite.cpp
@@ -24,12 +24,12 @@
 #include <string>   // for allocator, string
 #include <thread>   // for thread::id, thread
 
-#include "proteus/batching/batcher.hpp"      // for Batcher
-#include "proteus/buffers/buffer.hpp"        // IWYU pragma: keep
-#include "proteus/core/predict_api.hpp"      // for RequestParameters
-#include "proteus/core/worker_info.hpp"      // for WorkerInfo
-#include "proteus/helpers/declarations.hpp"  // for BufferPtrs
-#include "proteus/helpers/queue.hpp"         // for BufferPtrsQueue, BufferP...
+#include "proteus/batching/batcher.hpp"  // for Batcher
+#include "proteus/buffers/buffer.hpp"    // IWYU pragma: keep
+#include "proteus/core/predict_api.hpp"  // for RequestParameters
+#include "proteus/core/worker_info.hpp"  // for WorkerInfo
+#include "proteus/declarations.hpp"      // for BufferPtrs
+#include "proteus/util/queue.hpp"        // for BufferPtrsQueue, BufferP...
 
 namespace proteus {
 
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index e3b898fcd..73aca2aab 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -14,3 +14,5 @@
 
 add_subdirectory(batching)
 add_subdirectory(buffers)
+add_subdirectory(client_operators)
+add_subdirectory(core)
diff --git a/tests/unit/batching/test_soft_batching.cpp b/tests/unit/batching/test_soft_batching.cpp
index 128ea92d4..85c2e8d87 100644
--- a/tests/unit/batching/test_soft_batching.cpp
+++ b/tests/unit/batching/test_soft_batching.cpp
@@ -31,7 +31,7 @@
 #include "proteus/core/data_types.hpp"          // for DataType, DataType::U...
 #include "proteus/core/predict_api.hpp"         // for InferenceRequest, Req...
 #include "proteus/core/worker_info.hpp"         // for WorkerInfo
-#include "proteus/helpers/declarations.hpp"     // for BufferPtrs
+#include "proteus/declarations.hpp"             // for BufferPtrs
 #include "proteus/observation/logging.hpp"      // for initLogger, LogLevel
 
 namespace proteus {
diff --git a/tests/unit/buffers/test_vector_buffer.cpp b/tests/unit/buffers/test_vector_buffer.cpp
index 803164c66..cb4d001c1 100644
--- a/tests/unit/buffers/test_vector_buffer.cpp
+++ b/tests/unit/buffers/test_vector_buffer.cpp
@@ -19,8 +19,8 @@
 #include "gtest/gtest.h"                      // for EXPECT_EQ, FAIL, UnitTest
 #include "proteus/buffers/vector_buffer.hpp"  // for VectorBuffer
 #include "proteus/core/data_types.hpp"        // for DataType, getSize, oper...
-#include "proteus/helpers/declarations.hpp"   // for BufferPtrs
-#include "proteus/helpers/queue.hpp"          // for BufferPtrsQueue
+#include "proteus/declarations.hpp"           // for BufferPtrs
+#include "proteus/util/queue.hpp"             // for BufferPtrsQueue
 
 namespace proteus {
 
@@ -29,6 +29,30 @@ constexpr auto kBufferSize = 10;
 // constexpr auto kDataSize = types::getSize(kDataType);
 constexpr auto kTimeout = 1E6;  // 1E6 us = 1 s timeout
 
+struct WriteData {
+  template <typename T>
+  size_t operator()(Buffer* buffer, T value, size_t offset) const {
+    if constexpr (std::is_same_v<T, char>) {
+      (void)buffer, (void)value, (void)offset;
+      return offset;
+    } else {
+      return buffer->write(value, offset);
+    }
+  }
+};
+
+struct ReadData {
+  template <typename T>
+  void operator()(Buffer* buffer, size_t offset, T golden) const {
+    if constexpr (std::is_same_v<T, char>) {
+      (void)buffer, (void)offset, (void)golden;
+      FAIL() << "Unhandled datatype: char ";
+    } else {
+      EXPECT_EQ(*static_cast<T*>(buffer->data(offset)), golden);
+    }
+  }
+};
+
 class UnitVectorBufferFixture : public testing::TestWithParam<DataType> {
  public:
   UnitVectorBufferFixture() : buffer_(kBufferSize, GetParam()){};
@@ -37,44 +61,7 @@ class UnitVectorBufferFixture : public testing::TestWithParam<DataType> {
   void SetUp() override {
     size_t offset = 0;
     for (auto i = 0; i < kBufferSize; i++) {
-      switch (GetParam()) {
-        case DataType::BOOL:
-          offset = buffer_.write(false, offset);
-          break;
-        case DataType::UINT8:
-          offset = buffer_.write(static_cast<uint8_t>(0), offset);
-          break;
-        case DataType::UINT16:
-          offset = buffer_.write(static_cast<uint16_t>(0), offset);
-          break;
-        case DataType::UINT32:
-          offset = buffer_.write(static_cast<uint32_t>(0), offset);
-          break;
-        case DataType::UINT64:
-          offset = buffer_.write(static_cast<uint64_t>(0), offset);
-          break;
-        case DataType::INT8:
-          offset = buffer_.write(static_cast<int8_t>(0), offset);
-          break;
-        case DataType::INT16:
-          offset = buffer_.write(static_cast<int16_t>(0), offset);
-          break;
-        case DataType::INT32:
-          offset = buffer_.write(static_cast<int32_t>(0), offset);
-          break;
-        case DataType::INT64:
-          offset = buffer_.write(static_cast<int64_t>(0), offset);
-          break;
-        case DataType::FP32:
-          offset = buffer_.write(static_cast<float>(0), offset);
-          break;
-        case DataType::FP64:
-          offset = buffer_.write(static_cast<double>(0), offset);
-          break;
-        default:
-          FAIL() << "Unhandled datatype: " << GetParam();
-          break;
-      }
+      offset = switchOverTypes(WriteData(), GetParam(), &buffer_, i, offset);
     }
   }
 
@@ -86,66 +73,7 @@ class UnitVectorBufferFixture : public testing::TestWithParam<DataType> {
 TEST_P(UnitVectorBufferFixture, TestFixtureState) {
   auto datatype = GetParam();
   for (auto i = 0; i < kBufferSize; i++) {
-    switch (datatype) {
-      case DataType::BOOL: {
-        auto value = static_cast<bool*>(buffer_.data(i * datatype.size()));
-        EXPECT_EQ(*value, false);
-        break;
-      }
-      case DataType::UINT8: {
-        auto value = static_cast<uint8_t*>(buffer_.data(i * datatype.size()));
-        EXPECT_EQ(*value, static_cast<uint8_t>(0));
-        break;
-      }
-      case DataType::UINT16: {
-        auto value = static_cast<uint16_t*>(buffer_.data(i * datatype.size()));
-        EXPECT_EQ(*value, static_cast<uint16_t>(0));
-        break;
-      }
-      case DataType::UINT32: {
-        auto value = static_cast<uint32_t*>(buffer_.data(i * datatype.size()));
-        EXPECT_EQ(*value, static_cast<uint32_t>(0));
-        break;
-      }
-      case DataType::UINT64: {
-        auto value = static_cast<uint64_t*>(buffer_.data(i * datatype.size()));
-        EXPECT_EQ(*value, static_cast<uint64_t>(0));
-        break;
-      }
-      case DataType::INT8: {
-        auto value = static_cast<int8_t*>(buffer_.data(i * datatype.size()));
-        EXPECT_EQ(*value, static_cast<int8_t>(0));
-        break;
-      }
-      case DataType::INT16: {
-        auto value = static_cast<int16_t*>(buffer_.data(i * datatype.size()));
-        EXPECT_EQ(*value, static_cast<int16_t>(0));
-        break;
-      }
-      case DataType::INT32: {
-        auto value = static_cast<int32_t*>(buffer_.data(i * datatype.size()));
-        EXPECT_EQ(*value, static_cast<int32_t>(0));
-        break;
-      }
-      case DataType::INT64: {
-        auto value = static_cast<uint16_t*>(buffer_.data(i * datatype.size()));
-        EXPECT_EQ(*value, static_cast<uint16_t>(0));
-        break;
-      }
-      case DataType::FP32: {
-        auto value = static_cast<float*>(buffer_.data(i * datatype.size()));
-        EXPECT_EQ(*value, static_cast<float>(0));
-        break;
-      }
-      case DataType::FP64: {
-        auto value = static_cast<double*>(buffer_.data(i * datatype.size()));
-        EXPECT_EQ(*value, static_cast<double>(0));
-        break;
-      }
-      default:
-        FAIL() << "Unhandled datatype: " << GetParam();
-        break;
-    }
+    switchOverTypes(ReadData(), datatype, &buffer_, i * datatype.size(), i);
   }
 }
 
diff --git a/tests/unit/client_operators/CMakeLists.txt b/tests/unit/client_operators/CMakeLists.txt
new file mode 100644
index 000000000..119fa8a4e
--- /dev/null
+++ b/tests/unit/client_operators/CMakeLists.txt
@@ -0,0 +1,30 @@
+# Copyright 2022 Xilinx Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+include(GoogleTest)
+
+list(APPEND targets infer_async)
+
+foreach(target ${targets})
+  add_executable(test_${target} test_${target}.cpp)
+  target_include_directories(
+    test_${target} BEFORE PRIVATE ${PROTEUS_TEST_INCLUDE_DIRS}
+  )
+  target_include_directories(
+    test_${target} AFTER PRIVATE ${PROTEUS_INCLUDE_DIRS}
+  )
+  target_link_libraries(test_${target} proteus gtest gtest_main)
+
+  gtest_discover_tests(test_${target})
+endforeach()
diff --git a/tests/unit/client_operators/test_infer_async.cpp b/tests/unit/client_operators/test_infer_async.cpp
new file mode 100644
index 000000000..82e138661
--- /dev/null
+++ b/tests/unit/client_operators/test_infer_async.cpp
@@ -0,0 +1,54 @@
+// Copyright 2022 Xilinx Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cstddef>
+#include <iostream>
+
+#include "gtest/gtest.h"  // for Test, SuiteApiResolver, TEST
+#include "proteus/client_operators/infer_async.hpp"
+#include "proteus/clients/native.hpp"
+#include "proteus/core/predict_api.hpp"
+#include "proteus/testing/gtest_fixtures.hpp"  // for BaseFixture
+
+namespace proteus {
+
+TEST_F(HttpFixture, Ordered) {
+  NativeClient client;
+  auto endpoint = client.workerLoad("echo", nullptr);
+  EXPECT_EQ(endpoint, "echo");
+
+  std::vector<uint32_t> imgData;
+  const auto shape = {1UL};
+  const auto size = 1;
+  imgData.reserve(size);
+  imgData.push_back(1);
+
+  proteus::InferenceRequest request;
+  request.addInputTensor(imgData.data(), shape, proteus::DataType::UINT32);
+
+  const auto data_size = 10;
+  std::vector<InferenceRequest> reqs;
+  reqs.reserve(data_size);
+  for (auto i = 0; i < data_size; ++i) {
+    reqs.push_back(request);
+  }
+
+  auto resps = inferAsyncOrdered(&client, "echo", reqs);
+  EXPECT_EQ(resps.size(), data_size);
+  for (const auto& resp : resps) {
+    EXPECT_FALSE(resp.isError());
+  }
+}
+
+}  // namespace proteus
diff --git a/tests/unit/core/CMakeLists.txt b/tests/unit/core/CMakeLists.txt
new file mode 100644
index 000000000..2f301f628
--- /dev/null
+++ b/tests/unit/core/CMakeLists.txt
@@ -0,0 +1,30 @@
+# Copyright 2021 Xilinx Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+include(GoogleTest)
+
+list(APPEND targets inference_request_input request_parameters)
+
+foreach(target ${targets})
+  add_executable(test_${target} test_${target}.cpp)
+  target_include_directories(
+    test_${target} BEFORE PRIVATE ${PROTEUS_TEST_INCLUDE_DIRS}
+  )
+  target_include_directories(
+    test_${target} AFTER PRIVATE ${PROTEUS_INCLUDE_DIRS}
+  )
+  target_link_libraries(test_${target} predict_api gtest gtest_main)
+
+  gtest_discover_tests(test_${target})
+endforeach()
diff --git a/tests/unit/core/test_inference_request_input.cpp b/tests/unit/core/test_inference_request_input.cpp
new file mode 100644
index 000000000..8bfd0dbb7
--- /dev/null
+++ b/tests/unit/core/test_inference_request_input.cpp
@@ -0,0 +1,93 @@
+// Copyright 2022 Xilinx Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cstddef>
+#include <iostream>
+
+#include "gtest/gtest.h"  // for Test, SuiteApiResolver, TEST
+#include "proteus/core/predict_api.hpp"
+
+namespace proteus {
+
+TEST(UnitInferenceRequestInput, SerDes) {
+  const auto data_size = 10;
+  std::vector<char> data;
+  data.reserve(data_size);
+  for (auto i = 0; i < data_size; i++) {
+    data.push_back(static_cast<char>('a' + i));
+  }
+
+  InferenceRequestInput req(data.data(), {data_size}, DataType::UINT8, "test");
+
+  std::vector<std::byte> serial_data;
+  serial_data.reserve(req.serializeSize());
+  req.serialize(serial_data.data());
+
+  InferenceRequestInput new_req;
+  new_req.deserialize(serial_data.data());
+
+  EXPECT_EQ(req.getData(), new_req.getData());
+  bool type_match = req.getDatatype() == new_req.getDatatype();
+  EXPECT_TRUE(type_match);
+  EXPECT_EQ(req.getName(), new_req.getName());
+  auto old_shape = req.getShape();
+  auto new_shape = new_req.getShape();
+  const auto old_shape_size = old_shape.size();
+  EXPECT_EQ(old_shape_size, new_shape.size());
+  for (auto i = 0U; i < old_shape_size; ++i) {
+    EXPECT_EQ(old_shape[i], new_shape[i]);
+  }
+  EXPECT_EQ(req.getSize(), new_req.getSize());
+}
+
+TEST(UnitInferenceRequestInput, SerDes2) {
+  const auto data_size = 10;
+  std::vector<std::byte> data;
+  data.reserve(data_size);
+  for (auto i = 0; i < data_size; ++i) {
+    auto c = static_cast<std::byte>(static_cast<unsigned char>('a' + i));
+    // std::cout << c << " ";
+    data.push_back(c);
+  }
+
+  InferenceRequestInput req(nullptr, {data_size}, DataType::UINT8, "test");
+  req.setData(std::move(data));
+
+  std::vector<std::byte> serial_data;
+  serial_data.reserve(req.serializeSize());
+  req.serialize(serial_data.data());
+
+  InferenceRequestInput new_req;
+  new_req.deserialize(serial_data.data());
+
+  EXPECT_NE(req.getData(), new_req.getData());
+  const auto* old_data = static_cast<char*>(req.getData());
+  const auto* new_data = static_cast<char*>(new_req.getData());
+  for (int i = 0; i < data_size; i++) {
+    EXPECT_EQ(old_data[i], new_data[i]);
+  }
+  bool type_match = req.getDatatype() == new_req.getDatatype();
+  EXPECT_TRUE(type_match);
+  EXPECT_EQ(req.getName(), new_req.getName());
+  auto old_shape = req.getShape();
+  auto new_shape = new_req.getShape();
+  const auto old_shape_size = old_shape.size();
+  EXPECT_EQ(old_shape_size, new_shape.size());
+  for (auto i = 0U; i < old_shape_size; ++i) {
+    EXPECT_EQ(old_shape[i], new_shape[i]);
+  }
+  EXPECT_EQ(req.getSize(), new_req.getSize());
+}
+
+}  // namespace proteus
diff --git a/tests/unit/core/test_request_parameters.cpp b/tests/unit/core/test_request_parameters.cpp
new file mode 100644
index 000000000..bb44ac704
--- /dev/null
+++ b/tests/unit/core/test_request_parameters.cpp
@@ -0,0 +1,61 @@
+// Copyright 2022 Xilinx Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <cstddef>
+#include <iostream>
+
+#include "gtest/gtest.h"  // for Test, SuiteApiResolver, TEST
+#include "proteus/core/predict_api.hpp"
+
+namespace proteus {
+
+TEST(UnitRequestParameters, SerDes) {
+  std::array<std::string, 4> keys = {"string", "int", "bool", "double"};
+
+  RequestParameters params;
+  params.put(keys[0], "test");
+  params.put(keys[1], 4);
+  params.put(keys[2], false);
+  params.put(keys[3], 0.4);
+
+  auto size = params.serializeSize();
+  // 1 for number of parameters, plus 3 for each parameter: type index, length
+  // of the key, and length of the data
+  size_t expected_size = (1 + 3 * params.size()) * sizeof(size_t);
+  expected_size += keys[0].size() + (params.get<std::string>(keys[0]).size());
+  expected_size += keys[1].size() + sizeof(int);
+  expected_size += keys[2].size() + sizeof(bool);
+  expected_size += keys[3].size() + sizeof(double);
+  EXPECT_EQ(size, expected_size);
+
+  std::vector<std::byte> data;
+  data.reserve(size);
+  params.serialize(data.data());
+
+  RequestParameters new_params;
+  ASSERT_TRUE(new_params.empty());
+  new_params.deserialize(data.data());
+  EXPECT_EQ(params.size(), new_params.size());
+
+  for (const auto& key : keys) {
+    EXPECT_TRUE(new_params.has(key));
+  }
+  EXPECT_EQ(params.get<std::string>(keys[0]),
+            new_params.get<std::string>(keys[0]));
+  EXPECT_EQ(params.get<int>(keys[1]), new_params.get<int>(keys[1]));
+  EXPECT_EQ(params.get<bool>(keys[2]), new_params.get<bool>(keys[2]));
+  EXPECT_EQ(params.get<double>(keys[3]), new_params.get<double>(keys[3]));
+}
+
+}  // namespace proteus
diff --git a/tests/workers/test_echo.cpp b/tests/workers/test_echo.cpp
index 8adc13fe9..aa5ea70cf 100644
--- a/tests/workers/test_echo.cpp
+++ b/tests/workers/test_echo.cpp
@@ -86,9 +86,8 @@ class EchoParamFixture
 
     for (auto i = 0; i < multiplier; i++) {
       const auto& output = outputs[i];
-      auto* data = static_cast<std::vector<uint32_t>*>(output.getData());
-      EXPECT_EQ(data->size(), 1);
-      EXPECT_EQ((*data)[0], golden_outputs_[0]);
+      const auto* data = static_cast<uint32_t*>(output.getData());
+      EXPECT_EQ(data[0], golden_outputs_[0]);
       EXPECT_EQ(output.getDatatype(), DataType::UINT32);
       EXPECT_EQ(output.getName(), "echo");
       EXPECT_TRUE(output.getParameters()->empty());
diff --git a/tests/workers/test_facedetect.cpp b/tests/workers/test_facedetect.cpp
index ff73ba04a..44a698ab9 100644
--- a/tests/workers/test_facedetect.cpp
+++ b/tests/workers/test_facedetect.cpp
@@ -44,7 +44,7 @@ void dequeue_validate(FutureQueue& my_queue, int num_images) {
     auto outputs = results.getOutputs();
     EXPECT_EQ(outputs.size(), 1);
     for (auto& output : outputs) {
-      auto* data = static_cast<std::vector<float>*>(output.getData());
+      const auto* data = static_cast<float*>(output.getData());
       auto size = output.getSize();
       EXPECT_STREQ(output.getName().c_str(), "");
       EXPECT_STREQ(output.getDatatype().str(), "FP32");
@@ -60,7 +60,7 @@ void dequeue_validate(FutureQueue& my_queue, int num_images) {
       for (size_t i = 0; i < gold_response_size; i++) {
         // expect that the response values are within 1% of the golden
         const float abs_error = std::abs(gold_response_output[i] * 0.01);
-        EXPECT_NEAR((*data)[i], gold_response_output[i], abs_error);
+        EXPECT_NEAR(data[i], gold_response_output[i], abs_error);
       }
     }
   }
diff --git a/tests/workers/test_facedetect_stream.py b/tests/workers/test_facedetect_stream.py
index f8f9ff9fd..ebddf36f9 100644
--- a/tests/workers/test_facedetect_stream.py
+++ b/tests/workers/test_facedetect_stream.py
@@ -62,7 +62,10 @@ def construct_request(self, requested_frames_count):
 
         self.ws_client.modelInferWs(self.model, request)
         response_str = self.ws_client.modelRecv()
-        response = json.loads(response_str)
+        try:
+            response = json.loads(response_str)
+        except json.JSONDecodeError as e:
+            pytest.fail(f"Failed to parse as JSON: {response_str}", False)
 
         assert response["key"] == "0"
         assert float(response["data"]["img"]) == 15.0
diff --git a/tests/workers/test_invert_image.py b/tests/workers/test_invert_image.py
index a819ec045..e34dadc68 100644
--- a/tests/workers/test_invert_image.py
+++ b/tests/workers/test_invert_image.py
@@ -12,16 +12,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import argparse
+import base64
 import json
 import os
 
-import argparse
-import base64
 import cv2
 import numpy as np
 import pytest
+from helper import root_path, run_benchmark, run_benchmark_func
 
-from helper import run_benchmark, run_benchmark_func, root_path
 import proteus
 
 
@@ -35,7 +35,7 @@ def parameters_fixture():
     return None
 
 
-def compare_jpgs(test_image, reference_image, shape=None):
+def compare_jpgs(test_image, reference_image):
     """
     Compare a base64 encoded JPG image string with a reference image (as a np
     buffer). Since JPGs are lossy, images that are "equal" may be slightly
@@ -48,10 +48,8 @@ def compare_jpgs(test_image, reference_image, shape=None):
         shape (list, optional): Check that the labeled shape of the test image
             matches its actual decoded shape. Defaults to None.
     """
-    decoded_image = base64.decodebytes(str.encode(test_image))
+    decoded_image = base64.decodebytes(test_image)
     buffer = np.frombuffer(decoded_image, dtype=np.uint8)
-    if shape:
-        assert shape == [len(buffer)]
     received_image = cv2.imdecode(buffer, cv2.IMREAD_UNCHANGED)
     diff = received_image.astype(np.int16) - reference_image.astype(np.int16)
     m_norm = np.sum(np.absolute(diff))  # Manhattan norm
@@ -95,7 +93,7 @@ def send_request(self, request, image):
         output = outputs[0]
         if output.datatype == proteus.DataType.STRING:
             data = output.getStringData()
-            compare_jpgs(data, image, shape=output.shape)
+            compare_jpgs(data, image)
             assert output.parameters.empty()
         else:
             assert output.shape == [*image.shape]
diff --git a/tests/workers/test_invert_video.py b/tests/workers/test_invert_video.py
index 4c11c31e5..9cf4a7ae0 100644
--- a/tests/workers/test_invert_video.py
+++ b/tests/workers/test_invert_video.py
@@ -72,7 +72,7 @@ def recv_frames(self, video_path, count):
             resp_data = resp["data"]["img"].split(",")[1]
             _, frame = cap.read()
             frame = cv2.bitwise_not(frame)
-            compare_jpgs(resp_data, frame)
+            compare_jpgs(resp_data.encode(), frame)
 
     def test_invert_video_0(self):
         requested_frames_count = 100