diff --git a/src/python/grpcio/tests/qps/__init__.py b/src/python/grpcio/tests/qps/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..100a624dc9c1bbd89708c58e18cf2666a73f4cc7
--- /dev/null
+++ b/src/python/grpcio/tests/qps/__init__.py
@@ -0,0 +1,28 @@
+# Copyright 2016, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/src/python/grpcio/tests/qps/benchmark_client.py b/src/python/grpcio/tests/qps/benchmark_client.py
new file mode 100644
index 0000000000000000000000000000000000000000..eed0b0c6dac616137c1617965d657b8fee054751
--- /dev/null
+++ b/src/python/grpcio/tests/qps/benchmark_client.py
@@ -0,0 +1,186 @@
+# Copyright 2016, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Defines test client behaviors (UNARY/STREAMING) (SYNC/ASYNC)."""
+
+import abc
+import time
+try:
+  import Queue as queue  # Python 2.x
+except ImportError:
+  import queue  # Python 3
+
+from concurrent import futures
+
+from grpc.beta import implementations
+from src.proto.grpc.testing import messages_pb2
+from src.proto.grpc.testing import services_pb2
+from tests.unit import resources
+from tests.unit.beta import test_utilities
+
+_TIMEOUT = 60 * 60 * 24
+
+
+class BenchmarkClient:
+  """Benchmark client interface that exposes a non-blocking send_request()."""
+
+  __metaclass__ = abc.ABCMeta
+
+  def __init__(self, server, config, hist):
+    # Create the stub
+    host, port = server.split(':')
+    port = int(port)
+    if config.HasField('security_params'):
+      creds = implementations.ssl_channel_credentials(
+          resources.test_root_certificates())
+      channel = test_utilities.not_really_secure_channel(
+          host, port, creds, config.security_params.server_host_override)
+    else:
+      channel = implementations.insecure_channel(host, port)
+
+    if config.payload_config.WhichOneof('payload') == 'simple_params':
+      self._generic = False
+      self._stub = services_pb2.beta_create_BenchmarkService_stub(channel)
+      payload = messages_pb2.Payload(
+          body='\0' * config.payload_config.simple_params.req_size)
+      self._request = messages_pb2.SimpleRequest(
+          payload=payload,
+          response_size=config.payload_config.simple_params.resp_size)
+    else:
+      self._generic = True
+      self._stub = implementations.generic_stub(channel)
+      self._request = '\0' * config.payload_config.bytebuf_params.req_size
+
+    self._hist = hist
+    self._response_callbacks = []
+
+  def add_response_callback(self, callback):
+    self._response_callbacks.append(callback)
+
+  @abc.abstractmethod
+  def send_request(self):
+    """Non-blocking wrapper for a client's request operation."""
+    raise NotImplementedError()
+
+  def start(self):
+    pass
+
+  def stop(self):
+    pass
+
+  def _handle_response(self, query_time):
+    self._hist.add(query_time * 1e9)  # Report times in nanoseconds
+    for callback in self._response_callbacks:
+      callback(query_time)
+
+
+class UnarySyncBenchmarkClient(BenchmarkClient):
+
+  def __init__(self, server, config, hist):
+    super(UnarySyncBenchmarkClient, self).__init__(server, config, hist)
+    self._pool = futures.ThreadPoolExecutor(
+        max_workers=config.outstanding_rpcs_per_channel)
+
+  def send_request(self):
+    # Send requests in seperate threads to support multiple outstanding rpcs
+    # (See src/proto/grpc/testing/control.proto)
+    self._pool.submit(self._dispatch_request)
+
+  def stop(self):
+    self._pool.shutdown(wait=True)
+    self._stub = None
+
+  def _dispatch_request(self):
+    start_time = time.time()
+    self._stub.UnaryCall(self._request, _TIMEOUT)
+    end_time = time.time()
+    self._handle_response(end_time - start_time)
+
+
+class UnaryAsyncBenchmarkClient(BenchmarkClient):
+
+  def send_request(self):
+    # Use the Future callback api to support multiple outstanding rpcs
+    start_time = time.time()
+    response_future = self._stub.UnaryCall.future(self._request, _TIMEOUT)
+    response_future.add_done_callback(
+        lambda resp: self._response_received(start_time, resp))
+
+  def _response_received(self, start_time, resp):
+    resp.result()
+    end_time = time.time()
+    self._handle_response(end_time - start_time)
+
+  def stop(self):
+    self._stub = None
+
+
+class StreamingAsyncBenchmarkClient(BenchmarkClient):
+
+  def __init__(self, server, config, hist):
+    super(StreamingAsyncBenchmarkClient, self).__init__(server, config, hist)
+    self._is_streaming = False
+    self._pool = futures.ThreadPoolExecutor(max_workers=1)
+    # Use a thread-safe queue to put requests on the stream
+    self._request_queue = queue.Queue()
+    self._send_time_queue = queue.Queue()
+
+  def send_request(self):
+    self._send_time_queue.put(time.time())
+    self._request_queue.put(self._request)
+
+  def start(self):
+    self._is_streaming = True
+    self._pool.submit(self._request_stream)
+
+  def stop(self):
+    self._is_streaming = False
+    self._pool.shutdown(wait=True)
+    self._stub = None
+
+  def _request_stream(self):
+    self._is_streaming = True
+    if self._generic:
+      response_stream = self._stub.inline_stream_stream(
+          'grpc.testing.BenchmarkService', 'StreamingCall',
+          self._request_generator(), _TIMEOUT)
+    else:
+      response_stream = self._stub.StreamingCall(self._request_generator(),
+                                                 _TIMEOUT)
+    for _ in response_stream:
+      end_time = time.time()
+      self._handle_response(end_time - self._send_time_queue.get_nowait())
+
+  def _request_generator(self):
+    while self._is_streaming:
+      try:
+        request = self._request_queue.get(block=True, timeout=1.0)
+        yield request
+      except queue.Empty:
+        pass
diff --git a/src/python/grpcio/tests/qps/benchmark_server.py b/src/python/grpcio/tests/qps/benchmark_server.py
new file mode 100644
index 0000000000000000000000000000000000000000..8cbf480d58bb8798cfd0ed740414f44e66fdd041
--- /dev/null
+++ b/src/python/grpcio/tests/qps/benchmark_server.py
@@ -0,0 +1,58 @@
+# Copyright 2016, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from src.proto.grpc.testing import messages_pb2
+from src.proto.grpc.testing import services_pb2
+
+
+class BenchmarkServer(services_pb2.BetaBenchmarkServiceServicer):
+  """Synchronous Server implementation for the Benchmark service."""
+
+  def UnaryCall(self, request, context):
+    payload = messages_pb2.Payload(body='\0' * request.response_size)
+    return messages_pb2.SimpleResponse(payload=payload)
+
+  def StreamingCall(self, request_iterator, context):
+    for request in request_iterator:
+      payload = messages_pb2.Payload(body='\0' * request.response_size)
+      yield messages_pb2.SimpleResponse(payload=payload)
+
+
+class GenericBenchmarkServer(services_pb2.BetaBenchmarkServiceServicer):
+  """Generic Server implementation for the Benchmark service."""
+
+  def __init__(self, resp_size):
+    self._response = '\0' * resp_size
+
+  def UnaryCall(self, request, context):
+    return self._response
+
+  def StreamingCall(self, request_iterator, context):
+    for request in request_iterator:
+      yield self._response
diff --git a/src/python/grpcio/tests/qps/client_runner.py b/src/python/grpcio/tests/qps/client_runner.py
new file mode 100644
index 0000000000000000000000000000000000000000..a36c30ccc0a8a5862b42e5ef3238407955c8cdde
--- /dev/null
+++ b/src/python/grpcio/tests/qps/client_runner.py
@@ -0,0 +1,104 @@
+# Copyright 2016, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Defines behavior for WHEN clients send requests.
+
+Each client exposes a non-blocking send_request() method that the
+ClientRunner invokes either periodically or in response to some event.
+"""
+
+import abc
+import thread
+import time
+
+
+class ClientRunner:
+  """Abstract interface for sending requests from clients."""
+
+  __metaclass__ = abc.ABCMeta
+
+  def __init__(self, client):
+    self._client = client
+
+  @abc.abstractmethod
+  def start(self):
+    raise NotImplementedError()
+
+  @abc.abstractmethod
+  def stop(self):
+    raise NotImplementedError()
+
+
+class OpenLoopClientRunner(ClientRunner):
+
+  def __init__(self, client, interval_generator):
+    super(OpenLoopClientRunner, self).__init__(client)
+    self._is_running = False
+    self._interval_generator = interval_generator
+
+  def start(self):
+    self._is_running = True
+    self._client.start()
+    thread.start_new_thread(self._dispatch_requests, ())
+
+  def stop(self):
+    self._is_running = False
+    self._client.stop()
+    self._client = None
+
+  def _dispatch_requests(self):
+    while self._is_running:
+      self._client.send_request()
+      time.sleep(next(self._interval_generator))
+
+
+class ClosedLoopClientRunner(ClientRunner):
+
+  def __init__(self, client, request_count):
+    super(ClosedLoopClientRunner, self).__init__(client)
+    self._is_running = False
+    self._request_count = request_count
+    # Send a new request on each response for closed loop
+    self._client.add_response_callback(self._send_request)
+
+  def start(self):
+    self._is_running = True
+    for _ in xrange(self._request_count):
+      self._client.send_request()
+    self._client.start()
+
+  def stop(self):
+    self._is_running = False
+    self._client.stop()
+    self._client = None
+
+  def _send_request(self, response_time):
+    if self._is_running:
+      self._client.send_request()
+
diff --git a/src/python/grpcio/tests/qps/histogram.py b/src/python/grpcio/tests/qps/histogram.py
new file mode 100644
index 0000000000000000000000000000000000000000..9a7b5eb2ba622059245b26aeee54f56eced21630
--- /dev/null
+++ b/src/python/grpcio/tests/qps/histogram.py
@@ -0,0 +1,85 @@
+# Copyright 2016, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import math
+import threading
+
+from src.proto.grpc.testing import stats_pb2
+
+
+class Histogram(object):
+  """Histogram class used for recording performance testing data.
+
+  This class is thread safe.
+  """
+
+  def __init__(self, resolution, max_possible):
+    self._lock = threading.Lock()
+    self._resolution = resolution
+    self._max_possible = max_possible
+    self._sum = 0
+    self._sum_of_squares = 0
+    self.multiplier = 1.0 + self._resolution
+    self._count = 0
+    self._min = self._max_possible
+    self._max = 0
+    self._buckets = [0] * (self._bucket_for(self._max_possible) + 1)
+
+  def reset(self):
+    with self._lock:
+      self._sum = 0
+      self._sum_of_squares = 0
+      self._count = 0
+      self._min = self._max_possible
+      self._max = 0
+      self._buckets = [0] * (self._bucket_for(self._max_possible) + 1)
+
+  def add(self, val):
+    with self._lock:
+      self._sum += val
+      self._sum_of_squares += val * val
+      self._count += 1
+      self._min = min(self._min, val)
+      self._max = max(self._max, val)
+      self._buckets[self._bucket_for(val)] += 1
+
+  def get_data(self):
+    with self._lock:
+      data = stats_pb2.HistogramData()
+      data.bucket.extend(self._buckets)
+      data.min_seen = self._min
+      data.max_seen = self._max
+      data.sum = self._sum
+      data.sum_of_squares = self._sum_of_squares
+      data.count = self._count
+      return data
+
+  def _bucket_for(self, val):
+    val = min(val, self._max_possible)
+    return int(math.log(val, self.multiplier))
diff --git a/src/python/grpcio/tests/qps/qps_worker.py b/src/python/grpcio/tests/qps/qps_worker.py
new file mode 100644
index 0000000000000000000000000000000000000000..3dda718638e211b0499d6c207b5b4ddb8d55cc0c
--- /dev/null
+++ b/src/python/grpcio/tests/qps/qps_worker.py
@@ -0,0 +1,60 @@
+# Copyright 2016, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""The entry point for the qps worker."""
+
+import argparse
+import time
+
+from src.proto.grpc.testing import services_pb2
+
+from tests.qps import worker_server
+
+
+def run_worker_server(port):
+  servicer = worker_server.WorkerServer()
+  server = services_pb2.beta_create_WorkerService_server(servicer)
+  server.add_insecure_port('[::]:{}'.format(port))
+  server.start()
+  servicer.wait_for_quit()
+  # Drain outstanding requests for clean exit
+  time.sleep(2)
+  server.stop(0)
+
+
+if __name__ == '__main__':
+  parser = argparse.ArgumentParser(
+      description='gRPC Python performance testing worker')
+  parser.add_argument('--driver_port',
+                      type=int,
+                      dest='port',
+                      help='The port the worker should listen on')
+  args = parser.parse_args()
+
+  run_worker_server(args.port)
diff --git a/src/python/grpcio/tests/qps/worker_server.py b/src/python/grpcio/tests/qps/worker_server.py
new file mode 100644
index 0000000000000000000000000000000000000000..0b3acc14e7de785b7ee7ca70752f56e57a672a64
--- /dev/null
+++ b/src/python/grpcio/tests/qps/worker_server.py
@@ -0,0 +1,184 @@
+# Copyright 2016, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import multiprocessing
+import random
+import threading
+import time
+
+from grpc.beta import implementations
+from grpc.framework.interfaces.face import utilities
+from src.proto.grpc.testing import control_pb2
+from src.proto.grpc.testing import services_pb2
+from src.proto.grpc.testing import stats_pb2
+
+from tests.qps import benchmark_client
+from tests.qps import benchmark_server
+from tests.qps import client_runner
+from tests.qps import histogram
+from tests.unit import resources
+
+
+class WorkerServer(services_pb2.BetaWorkerServiceServicer):
+  """Python Worker Server implementation."""
+
+  def __init__(self):
+    self._quit_event = threading.Event()
+
+  def RunServer(self, request_iterator, context):
+    config = next(request_iterator).setup
+    server, port = self._create_server(config)
+    cores = multiprocessing.cpu_count()
+    server.start()
+    start_time = time.time()
+    yield self._get_server_status(start_time, start_time, port, cores)
+
+    for request in request_iterator:
+      end_time = time.time()
+      status = self._get_server_status(start_time, end_time, port, cores)
+      if request.mark.reset:
+        start_time = end_time
+      yield status
+    server.stop(0)
+
+  def _get_server_status(self, start_time, end_time, port, cores):
+    end_time = time.time()
+    elapsed_time = end_time - start_time
+    stats = stats_pb2.ServerStats(time_elapsed=elapsed_time,
+                                  time_user=elapsed_time,
+                                  time_system=elapsed_time)
+    return control_pb2.ServerStatus(stats=stats, port=port, cores=cores)
+
+  def _create_server(self, config):
+    if config.server_type == control_pb2.SYNC_SERVER:
+      servicer = benchmark_server.BenchmarkServer()
+      server = services_pb2.beta_create_BenchmarkService_server(servicer)
+    elif config.server_type == control_pb2.ASYNC_GENERIC_SERVER:
+      resp_size = config.payload_config.bytebuf_params.resp_size
+      servicer = benchmark_server.GenericBenchmarkServer(resp_size)
+      method_implementations = {
+          ('grpc.testing.BenchmarkService', 'StreamingCall'):
+          utilities.stream_stream_inline(servicer.StreamingCall),
+          ('grpc.testing.BenchmarkService', 'UnaryCall'):
+          utilities.unary_unary_inline(servicer.UnaryCall),
+      }
+      server = implementations.server(method_implementations)
+    else:
+      raise Exception('Unsupported server type {}'.format(config.server_type))
+
+    if config.HasField('security_params'):  # Use SSL
+      server_creds = implementations.ssl_server_credentials([(
+          resources.private_key(), resources.certificate_chain())])
+      port = server.add_secure_port('[::]:{}'.format(config.port), server_creds)
+    else:
+      port = server.add_insecure_port('[::]:{}'.format(config.port))
+
+    return (server, port)
+
+  def RunClient(self, request_iterator, context):
+    config = next(request_iterator).setup
+    client_runners = []
+    qps_data = histogram.Histogram(config.histogram_params.resolution,
+                                   config.histogram_params.max_possible)
+    start_time = time.time()
+
+    # Create a client for each channel
+    for i in xrange(config.client_channels):
+      server = config.server_targets[i % len(config.server_targets)]
+      runner = self._create_client_runner(server, config, qps_data)
+      client_runners.append(runner)
+      runner.start()
+
+    end_time = time.time()
+    yield self._get_client_status(start_time, end_time, qps_data)
+
+    # Respond to stat requests
+    for request in request_iterator:
+      end_time = time.time()
+      status = self._get_client_status(start_time, end_time, qps_data)
+      if request.mark.reset:
+        qps_data.reset()
+        start_time = time.time()
+      yield status
+
+    # Cleanup the clients
+    for runner in client_runners:
+      runner.stop()
+
+  def _get_client_status(self, start_time, end_time, qps_data):
+    latencies = qps_data.get_data()
+    end_time = time.time()
+    elapsed_time = end_time - start_time
+    stats = stats_pb2.ClientStats(latencies=latencies,
+                                  time_elapsed=elapsed_time,
+                                  time_user=elapsed_time,
+                                  time_system=elapsed_time)
+    return control_pb2.ClientStatus(stats=stats)
+
+  def _create_client_runner(self, server, config, qps_data):
+    if config.client_type == control_pb2.SYNC_CLIENT:
+      if config.rpc_type == control_pb2.UNARY:
+        client = benchmark_client.UnarySyncBenchmarkClient(
+            server, config, qps_data)
+      else:
+        raise Exception('STREAMING SYNC client not supported')
+    elif config.client_type == control_pb2.ASYNC_CLIENT:
+      if config.rpc_type == control_pb2.UNARY:
+        client = benchmark_client.UnaryAsyncBenchmarkClient(
+            server, config, qps_data)
+      elif config.rpc_type == control_pb2.STREAMING:
+        client = benchmark_client.StreamingAsyncBenchmarkClient(
+            server, config, qps_data)
+    else:
+      raise Exception('Unsupported client type {}'.format(config.client_type))
+
+    # In multi-channel tests, we split the load across all channels
+    load_factor = float(config.client_channels)
+    if config.load_params.WhichOneof('load') == 'closed_loop':
+      runner = client_runner.ClosedLoopClientRunner(
+          client, config.outstanding_rpcs_per_channel)
+    else:  # Open loop Poisson
+      alpha = config.load_params.poisson.offered_load / load_factor
+      def poisson():
+        while True:
+          yield random.expovariate(alpha)
+
+      runner = client_runner.OpenLoopClientRunner(client, poisson())
+
+    return runner
+
+  def CoreCount(self, request, context):
+    return control_pb2.CoreResponse(cores=multiprocessing.cpu_count())
+
+  def QuitWorker(self, request, context):
+    self._quit_event.set()
+    return control_pb2.Void()
+
+  def wait_for_quit(self):
+    self._quit_event.wait()
diff --git a/tools/gce/linux_performance_worker_init.sh b/tools/gce/linux_performance_worker_init.sh
index 478e04ef37095e7565c3770d5de274050714fe2f..25ac3bcedea29789148f7497e5f098546315bb84 100755
--- a/tools/gce/linux_performance_worker_init.sh
+++ b/tools/gce/linux_performance_worker_init.sh
@@ -83,11 +83,13 @@ sudo apt-get install -y libgflags-dev libgtest-dev libc++-dev clang
 # Python dependencies
 sudo pip install tabulate
 sudo pip install google-api-python-client
+sudo pip install tox
 
 curl -O https://bootstrap.pypa.io/get-pip.py
 sudo pypy get-pip.py
 sudo pypy -m pip install tabulate
 sudo pip install google-api-python-client
+sudo pip install tox
 
 # Node dependencies (nvm has to be installed under user jenkins)
 touch .profile
diff --git a/tools/jenkins/run_performance.sh b/tools/jenkins/run_performance.sh
index 903a14421512889660e17c16941ad93a1d65894b..13a332751b22bd5b0f532282ee5213d9dbb50d44 100755
--- a/tools/jenkins/run_performance.sh
+++ b/tools/jenkins/run_performance.sh
@@ -34,4 +34,4 @@ set -ex
 # Enter the gRPC repo root
 cd $(dirname $0)/../..
 
-tools/run_tests/run_performance_tests.py -l c++ node ruby csharp
+tools/run_tests/run_performance_tests.py -l c++ node ruby csharp python
diff --git a/tools/run_tests/performance/run_worker_python.sh b/tools/run_tests/performance/run_worker_python.sh
new file mode 100755
index 0000000000000000000000000000000000000000..0da8deda58e909deaacdf88d8cf891e61a862fe1
--- /dev/null
+++ b/tools/run_tests/performance/run_worker_python.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+# Copyright 2016, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#     * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#     * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+set -ex
+
+cd $(dirname $0)/../../..
+
+PYTHONPATH=src/python/grpcio:src/python/gens .tox/py27/bin/python src/python/grpcio/tests/qps/qps_worker.py $@
diff --git a/tools/run_tests/performance/scenario_config.py b/tools/run_tests/performance/scenario_config.py
index 55657f8d8a2dcd57da8105cda87fe40b0fb12c21..ddbe2375691b01f21890b71891eb98d18825769a 100644
--- a/tools/run_tests/performance/scenario_config.py
+++ b/tools/run_tests/performance/scenario_config.py
@@ -456,6 +456,128 @@ class NodeLanguage:
   def __str__(self):
     return 'node'
 
+class PythonLanguage:
+
+  def __init__(self):
+    self.safename = 'python'
+
+  def worker_cmdline(self):
+    return ['tools/run_tests/performance/run_worker_python.sh']
+
+  def worker_port_offset(self):
+    return 500
+
+  def scenarios(self):
+    yield {
+        'name': 'python_to_cpp_protobuf_streaming_ping_pong',
+        'num_servers': 1,
+        'num_clients': 1,
+        'client_config': {
+          'client_type': 'ASYNC_CLIENT',
+          'security_params': SECURE_SECARGS,
+          'outstanding_rpcs_per_channel': 1,
+          'client_channels': 1,
+          'async_client_threads': 1,
+          'rpc_type': 'STREAMING',
+          'load_params': {
+            'closed_loop': {}
+          },
+          'payload_config': EMPTY_PROTO_PAYLOAD,
+          'histogram_params': HISTOGRAM_PARAMS,
+        },
+        'server_config': {
+          'server_type': 'SYNC_SERVER',
+          'security_params': SECURE_SECARGS,
+          'core_limit': 0,
+          'async_server_threads': 1,
+        },
+        'warmup_seconds': WARMUP_SECONDS,
+        'benchmark_seconds': BENCHMARK_SECONDS,
+        'SERVER_LANGUAGE': 'c++' 
+    }
+    yield {
+        'name': 'python_protobuf_sync_unary_ping_pong',
+        'num_servers': 1,
+        'num_clients': 1,
+        'client_config': {
+          'client_type': 'SYNC_CLIENT',
+          'security_params': SECURE_SECARGS,
+          'outstanding_rpcs_per_channel': 1,
+          'client_channels': 1,
+          'async_client_threads': 1,
+          'rpc_type': 'UNARY',
+          'load_params': {
+            'closed_loop': {}
+          },
+          'payload_config': EMPTY_PROTO_PAYLOAD,
+          'histogram_params': HISTOGRAM_PARAMS,
+        },
+        'server_config': {
+          'server_type': 'SYNC_SERVER',
+          'security_params': SECURE_SECARGS,
+          'core_limit': 0,
+          'async_server_threads': 1,
+        },
+        'warmup_seconds': WARMUP_SECONDS,
+        'benchmark_seconds': BENCHMARK_SECONDS,
+    }
+    yield {
+        'name': 'python_protobuf_async_unary_ping_pong',
+        'num_servers': 1,
+        'num_clients': 1,
+        'client_config': {
+          'client_type': 'ASYNC_CLIENT',
+          'security_params': SECURE_SECARGS,
+          'outstanding_rpcs_per_channel': 1,
+          'client_channels': 1,
+          'async_client_threads': 1,
+            'rpc_type': 'UNARY',
+            'load_params': {
+              'closed_loop': {}
+            },
+            'payload_config': EMPTY_PROTO_PAYLOAD,
+            'histogram_params': HISTOGRAM_PARAMS,
+          },
+          'server_config': {
+            'server_type': 'SYNC_SERVER',
+            'security_params': SECURE_SECARGS,
+            'core_limit': 0,
+            'async_server_threads': 1,
+          },
+          'warmup_seconds': WARMUP_SECONDS,
+          'benchmark_seconds': BENCHMARK_SECONDS,
+    }
+    yield {
+        'name': 'python_to_cpp_single_channel_throughput',
+        'num_servers': 1,
+        'num_clients': 1,
+        'client_config': {
+          'client_type': 'ASYNC_CLIENT',
+          'security_params': SECURE_SECARGS,
+          'outstanding_rpcs_per_channel': 1,
+          'client_channels': 1,
+          'async_client_threads': 1,
+          'rpc_type': 'STREAMING',
+          'load_params': {
+            'closed_loop': {}
+          },
+          'payload_config': BIG_GENERIC_PAYLOAD,
+          'histogram_params': HISTOGRAM_PARAMS,
+        },
+        'server_config': {
+          'server_type': 'ASYNC_GENERIC_SERVER',
+          'security_params': SECURE_SECARGS,
+          'core_limit': SINGLE_MACHINE_CORES/2,
+          'async_server_threads': 1,
+          'payload_config': BIG_GENERIC_PAYLOAD,
+        },
+        'warmup_seconds': WARMUP_SECONDS,
+        'benchmark_seconds': BENCHMARK_SECONDS,
+        'SERVER_LANGUAGE': 'c++'
+    }
+      
+  def __str__(self):
+    return 'python'
 
 class RubyLanguage:
 
@@ -562,4 +684,5 @@ LANGUAGES = {
     'node' : NodeLanguage(),
     'ruby' : RubyLanguage(),
     'java' : JavaLanguage(),
+    'python' : PythonLanguage(),
 }
diff --git a/tools/run_tests/run_performance_tests.py b/tools/run_tests/run_performance_tests.py
index ada341abf54bcfbc27e7cb026dd9ce1d88f4a6b9..485265311e3dabeff5bc454e4ac6246ced27668f 100755
--- a/tools/run_tests/run_performance_tests.py
+++ b/tools/run_tests/run_performance_tests.py
@@ -238,6 +238,9 @@ def start_qpsworkers(languages, worker_hosts):
 def create_scenarios(languages, workers_by_lang, remote_host=None, regex='.*',
                      bq_result_table=None):
   """Create jobspecs for scenarios to run."""
+  all_workers = [worker
+                 for workers in workers_by_lang.values()
+                 for worker in workers]
   scenarios = []
   for language in languages:
     for scenario_json in language.scenarios():
@@ -263,9 +266,6 @@ def create_scenarios(languages, workers_by_lang, remote_host=None, regex='.*',
         scenarios.append(scenario)
 
   # the very last scenario requests shutting down the workers.
-  all_workers = [worker
-                 for workers in workers_by_lang.values()
-                 for worker in workers]
   scenarios.append(create_quit_jobspec(all_workers, remote_host=remote_host))
   return scenarios