diff --git a/CHANGELOG.md b/CHANGELOG.md
index feb865a13c..c8d2a28508 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fix weak reference error for pyodbc cursor in SQLAlchemy instrumentation.
   ([#469](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/469))
 
+### Added
+- `opentelemetry-instrumentation-httpx` Add `httpx` instrumentation
+  ([#461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/461))
+
 ## [0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01
 
 ### Changed
diff --git a/docs-requirements.txt b/docs-requirements.txt
index 5dafd0e0eb..09eaa24c08 100644
--- a/docs-requirements.txt
+++ b/docs-requirements.txt
@@ -34,3 +34,4 @@ redis>=2.6
 sqlalchemy>=1.0
 tornado>=6.0
 ddtrace>=0.34.0
+httpx~=0.18.0
\ No newline at end of file
diff --git a/docs/instrumentation/httpx/httpx.rst b/docs/instrumentation/httpx/httpx.rst
new file mode 100644
index 0000000000..f816539f5b
--- /dev/null
+++ b/docs/instrumentation/httpx/httpx.rst
@@ -0,0 +1,10 @@
+.. include:: ../../../instrumentation/opentelemetry-instrumentation-httpx/README.rst
+    :end-before: References
+
+API
+---
+
+.. automodule:: opentelemetry.instrumentation.httpx
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/nitpick-exceptions.ini b/docs/nitpick-exceptions.ini
index 268763e0c6..72a1be5461 100644
--- a/docs/nitpick-exceptions.ini
+++ b/docs/nitpick-exceptions.ini
@@ -18,6 +18,13 @@ class_references=
     Setter
     Getter
     ; - AwsXRayFormat.extract
+    ; httpx changes __module__ causing Sphinx to error and no Sphinx site is available
+    httpx.Client
+    httpx.AsyncClient
+    httpx.BaseTransport
+    httpx.AsyncBaseTransport
+    httpx.SyncByteStream
+    httpx.AsyncByteStream
 
 anys=
     ; API
@@ -36,3 +43,4 @@ anys=
     BaseInstrumentor
     ; - instrumentation.*
     Setter
+    httpx
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/LICENSE b/instrumentation/opentelemetry-instrumentation-httpx/LICENSE
new file mode 100644
index 0000000000..1ef7dad2c5
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright The OpenTelemetry Authors
+
+   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.
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/MANIFEST.in b/instrumentation/opentelemetry-instrumentation-httpx/MANIFEST.in
new file mode 100644
index 0000000000..aed3e33273
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/MANIFEST.in
@@ -0,0 +1,9 @@
+graft src
+graft tests
+global-exclude *.pyc
+global-exclude *.pyo
+global-exclude __pycache__/*
+include CHANGELOG.md
+include MANIFEST.in
+include README.rst
+include LICENSE
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/README.rst b/instrumentation/opentelemetry-instrumentation-httpx/README.rst
new file mode 100644
index 0000000000..722f1d2bea
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/README.rst
@@ -0,0 +1,170 @@
+OpenTelemetry HTTPX Instrumentation
+===================================
+
+|pypi|
+
+.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-httpx.svg
+   :target: https://pypi.org/project/opentelemetry-instrumentation-httpx/
+
+This library allows tracing HTTP requests made by the
+`httpx <https://www.python-httpx.org/>`_ library.
+
+Installation
+------------
+
+::
+
+     pip install opentelemetry-instrumentation-httpx
+
+
+Usage
+-----
+
+Instrumenting all clients
+*************************
+
+When using the instrumentor, all clients will automatically trace requests.
+
+.. code-block:: python
+
+     import httpx
+     from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
+
+     url = "https://httpbin.org/get"
+     HTTPXClientInstrumentor().instrument()
+
+     with httpx.Client() as client:
+          response = client.get(url)
+
+     async with httpx.AsyncClient() as client:
+          response = await client.get(url)
+
+Instrumenting single clients
+****************************
+
+If you only want to instrument requests for specific client instances, you can
+use the `instrument_client` method.
+
+
+.. code-block:: python
+
+    import httpx
+    from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
+
+    url = "https://httpbin.org/get"
+
+    with httpx.Client(transport=telemetry_transport) as client:
+        HTTPXClientInstrumentor.instrument_client(client)
+        response = client.get(url)
+
+    async with httpx.AsyncClient(transport=telemetry_transport) as client:
+        HTTPXClientInstrumentor.instrument_client(client)
+        response = await client.get(url)
+
+
+Uninstrument
+************
+
+If you need to uninstrument clients, there are two options available.
+
+.. code-block:: python
+
+     import httpx
+     from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
+
+     HTTPXClientInstrumentor().instrument()
+     client = httpx.Client()
+
+     # Uninstrument a specific client
+     HTTPXClientInstrumentor.uninstrument_client(client)
+     
+     # Uninstrument all clients
+     HTTPXClientInstrumentor().uninstrument()
+
+
+Using transports directly
+*************************
+
+If you don't want to use the instrumentor class, you can use the transport classes directly.
+
+
+.. code-block:: python
+
+    import httpx
+    from opentelemetry.instrumentation.httpx import (
+        AsyncOpenTelemetryTransport,
+        SyncOpenTelemetryTransport,
+    )
+
+    url = "https://httpbin.org/get"
+    transport = httpx.HTTPTransport()
+    telemetry_transport = SyncOpenTelemetryTransport(transport)
+
+    with httpx.Client(transport=telemetry_transport) as client:
+        response = client.get(url)
+
+    transport = httpx.AsyncHTTPTransport()
+    telemetry_transport = AsyncOpenTelemetryTransport(transport)
+
+    async with httpx.AsyncClient(transport=telemetry_transport) as client:
+        response = await client.get(url)
+
+
+Request and response hooks
+***************************
+
+The instrumentation supports specifying request and response hooks. These are functions that get called back by the instrumentation right after a span is created for a request
+and right before the span is finished while processing a response.
+
+.. note::
+
+    The request hook receives the raw arguments provided to the transport layer. The response hook receives the raw return values from the transport layer.
+
+The hooks can be configured as follows:
+
+
+.. code-block:: python
+
+    from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
+
+    def request_hook(span, request):
+        # method, url, headers, stream, extensions = request
+        pass
+
+    def response_hook(span, request, response):
+        # method, url, headers, stream, extensions = request
+        # status_code, headers, stream, extensions = response
+        pass
+
+    HTTPXClientInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook)
+
+
+Or if you are using the transport classes directly:
+
+
+.. code-block:: python
+
+    from opentelemetry.instrumentation.httpx import SyncOpenTelemetryTransport
+
+    def request_hook(span, request):
+        # method, url, headers, stream, extensions = request
+        pass
+
+    def response_hook(span, request, response):
+        # method, url, headers, stream, extensions = request
+        # status_code, headers, stream, extensions = response
+        pass
+
+    transport = httpx.HTTPTransport()
+    telemetry_transport = SyncOpenTelemetryTransport(
+        transport,
+        request_hook=request_hook,
+        response_hook=response_hook
+    )
+
+
+References
+----------
+
+* `OpenTelemetry HTTPX Instrumentation <https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/httpx/httpx.html>`_
+* `OpenTelemetry Project <https://opentelemetry.io/>`_
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/setup.cfg b/instrumentation/opentelemetry-instrumentation-httpx/setup.cfg
new file mode 100644
index 0000000000..cdc5dad7d9
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/setup.cfg
@@ -0,0 +1,58 @@
+# Copyright The OpenTelemetry Authors
+#
+# 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.
+#
+[metadata]
+name = opentelemetry-instrumentation-httpx
+description = OpenTelemetry HTTPX Instrumentation
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+author = OpenTelemetry Authors
+author_email = cncf-opentelemetry-contributors@lists.cncf.io
+url = https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-httpx
+platforms = any
+license = Apache-2.0
+classifiers =
+    Development Status :: 4 - Beta
+    Intended Audience :: Developers
+    License :: OSI Approved :: Apache Software License
+    Programming Language :: Python
+    Programming Language :: Python :: 3
+    Programming Language :: Python :: 3.6
+    Programming Language :: Python :: 3.7
+    Programming Language :: Python :: 3.8
+    Programming Language :: Python :: 3.9
+
+[options]
+python_requires = >=3.6
+package_dir=
+    =src
+packages=find_namespace:
+install_requires =
+    opentelemetry-api == 1.4.0.dev0
+    opentelemetry-instrumentation == 0.23.dev0
+    opentelemetry-semantic-conventions == 0.23.dev0
+    wrapt >= 1.0.0, < 2.0.0
+
+[options.extras_require]
+test =
+    opentelemetry-sdk == 1.4.0.dev0
+    opentelemetry-test == 0.23.dev0
+    respx ~= 0.17.0
+
+[options.packages.find]
+where = src
+
+[options.entry_points]
+opentelemetry_instrumentor =
+    httpx = opentelemetry.instrumentation.httpx:HTTPXClientInstrumentor
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/setup.py b/instrumentation/opentelemetry-instrumentation-httpx/setup.py
new file mode 100644
index 0000000000..26a48970fa
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/setup.py
@@ -0,0 +1,89 @@
+# Copyright The OpenTelemetry Authors
+#
+# 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.
+
+
+# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM templates/instrumentation_setup.py.txt.
+# RUN `python scripts/generate_setup.py` TO REGENERATE.
+
+
+import distutils.cmd
+import json
+import os
+from configparser import ConfigParser
+
+import setuptools
+
+config = ConfigParser()
+config.read("setup.cfg")
+
+# We provide extras_require parameter to setuptools.setup later which
+# overwrites the extra_require section from setup.cfg. To support extra_require
+# secion in setup.cfg, we load it here and merge it with the extra_require param.
+extras_require = {}
+if "options.extras_require" in config:
+    for key, value in config["options.extras_require"].items():
+        extras_require[key] = [v for v in value.split("\n") if v.strip()]
+
+BASE_DIR = os.path.dirname(__file__)
+PACKAGE_INFO = {}
+
+VERSION_FILENAME = os.path.join(
+    BASE_DIR, "src", "opentelemetry", "instrumentation", "httpx", "version.py"
+)
+with open(VERSION_FILENAME) as f:
+    exec(f.read(), PACKAGE_INFO)
+
+PACKAGE_FILENAME = os.path.join(
+    BASE_DIR, "src", "opentelemetry", "instrumentation", "httpx", "package.py"
+)
+with open(PACKAGE_FILENAME) as f:
+    exec(f.read(), PACKAGE_INFO)
+
+# Mark any instruments/runtime dependencies as test dependencies as well.
+extras_require["instruments"] = PACKAGE_INFO["_instruments"]
+test_deps = extras_require.get("test", [])
+for dep in extras_require["instruments"]:
+    test_deps.append(dep)
+
+extras_require["test"] = test_deps
+
+
+class JSONMetadataCommand(distutils.cmd.Command):
+
+    description = (
+        "print out package metadata as JSON. This is used by OpenTelemetry dev scripts to ",
+        "auto-generate code in other places",
+    )
+    user_options = []
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        metadata = {
+            "name": config["metadata"]["name"],
+            "version": PACKAGE_INFO["__version__"],
+            "instruments": PACKAGE_INFO["_instruments"],
+        }
+        print(json.dumps(metadata))
+
+
+setuptools.setup(
+    cmdclass={"meta": JSONMetadataCommand},
+    version=PACKAGE_INFO["__version__"],
+    extras_require=extras_require,
+)
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py
new file mode 100644
index 0000000000..fa3d29faf2
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/__init__.py
@@ -0,0 +1,414 @@
+# Copyright The OpenTelemetry Authors
+#
+# 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.
+
+import typing
+
+import httpx
+import wrapt
+
+from opentelemetry import context
+from opentelemetry.instrumentation.httpx.package import _instruments
+from opentelemetry.instrumentation.httpx.version import __version__
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.instrumentation.utils import (
+    http_status_to_status_code,
+    unwrap,
+)
+from opentelemetry.propagate import inject
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.trace import SpanKind, Tracer, TracerProvider, get_tracer
+from opentelemetry.trace.span import Span
+from opentelemetry.trace.status import Status
+
+URL = typing.Tuple[bytes, bytes, typing.Optional[int], bytes]
+Headers = typing.List[typing.Tuple[bytes, bytes]]
+RequestHook = typing.Callable[[Span, "RequestInfo"], None]
+ResponseHook = typing.Callable[[Span, "RequestInfo", "ResponseInfo"], None]
+AsyncRequestHook = typing.Callable[
+    [Span, "RequestInfo"], typing.Awaitable[typing.Any]
+]
+AsyncResponseHook = typing.Callable[
+    [Span, "RequestInfo", "ResponseInfo"], typing.Awaitable[typing.Any]
+]
+
+
+class RequestInfo(typing.NamedTuple):
+    method: bytes
+    url: URL
+    headers: typing.Optional[Headers]
+    stream: typing.Optional[
+        typing.Union[httpx.SyncByteStream, httpx.AsyncByteStream]
+    ]
+    extensions: typing.Optional[dict]
+
+
+class ResponseInfo(typing.NamedTuple):
+    status_code: int
+    headers: typing.Optional[Headers]
+    stream: typing.Iterable[bytes]
+    extensions: typing.Optional[dict]
+
+
+def _get_default_span_name(method: str) -> str:
+    return "HTTP {}".format(method).strip()
+
+
+def _apply_status_code(span: Span, status_code: int) -> None:
+    if not span.is_recording():
+        return
+
+    span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
+    span.set_status(Status(http_status_to_status_code(status_code)))
+
+
+def _prepare_attributes(method: bytes, url: URL) -> typing.Dict[str, str]:
+    _method = method.decode().upper()
+    _url = str(httpx.URL(url))
+    span_attributes = {
+        SpanAttributes.HTTP_METHOD: _method,
+        SpanAttributes.HTTP_URL: _url,
+    }
+    return span_attributes
+
+
+def _prepare_headers(headers: typing.Optional[Headers]) -> httpx.Headers:
+    return httpx.Headers(headers)
+
+
+class SyncOpenTelemetryTransport(httpx.BaseTransport):
+    """Sync transport class that will trace all requests made with a client.
+
+    Args:
+        transport: SyncHTTPTransport instance to wrap
+        tracer_provider: Tracer provider to use
+        request_hook: A hook that receives the span and request that is called
+            right after the span is created
+        response_hook: A hook that receives the span, request, and response
+            that is called right before the span ends
+    """
+
+    def __init__(
+        self,
+        transport: httpx.BaseTransport,
+        tracer_provider: typing.Optional[TracerProvider] = None,
+        request_hook: typing.Optional[RequestHook] = None,
+        response_hook: typing.Optional[ResponseHook] = None,
+    ):
+        self._transport = transport
+        self._tracer = get_tracer(
+            __name__,
+            instrumenting_library_version=__version__,
+            tracer_provider=tracer_provider,
+        )
+        self._request_hook = request_hook
+        self._response_hook = response_hook
+
+    def handle_request(
+        self,
+        method: bytes,
+        url: URL,
+        headers: typing.Optional[Headers] = None,
+        stream: typing.Optional[httpx.SyncByteStream] = None,
+        extensions: typing.Optional[dict] = None,
+    ) -> typing.Tuple[int, "Headers", httpx.SyncByteStream, dict]:
+        """Add request info to span."""
+        if context.get_value("suppress_instrumentation"):
+            return self._transport.handle_request(
+                method,
+                url,
+                headers=headers,
+                stream=stream,
+                extensions=extensions,
+            )
+
+        span_attributes = _prepare_attributes(method, url)
+        _headers = _prepare_headers(headers)
+        span_name = _get_default_span_name(
+            span_attributes[SpanAttributes.HTTP_METHOD]
+        )
+        request = RequestInfo(method, url, headers, stream, extensions)
+
+        with self._tracer.start_as_current_span(
+            span_name, kind=SpanKind.CLIENT, attributes=span_attributes
+        ) as span:
+            if self._request_hook is not None:
+                self._request_hook(span, request)
+
+            inject(_headers)
+
+            (
+                status_code,
+                headers,
+                stream,
+                extensions,
+            ) = self._transport.handle_request(
+                method,
+                url,
+                headers=_headers.raw,
+                stream=stream,
+                extensions=extensions,
+            )
+
+            _apply_status_code(span, status_code)
+
+            if self._response_hook is not None:
+                self._response_hook(
+                    span,
+                    request,
+                    ResponseInfo(status_code, headers, stream, extensions),
+                )
+
+        return status_code, headers, stream, extensions
+
+
+class AsyncOpenTelemetryTransport(httpx.AsyncBaseTransport):
+    """Async transport class that will trace all requests made with a client.
+
+    Args:
+        transport: AsyncHTTPTransport instance to wrap
+        tracer_provider: Tracer provider to use
+        request_hook: A hook that receives the span and request that is called
+            right after the span is created
+        response_hook: A hook that receives the span, request, and response
+            that is called right before the span ends
+    """
+
+    def __init__(
+        self,
+        transport: httpx.AsyncBaseTransport,
+        tracer_provider: typing.Optional[TracerProvider] = None,
+        request_hook: typing.Optional[RequestHook] = None,
+        response_hook: typing.Optional[ResponseHook] = None,
+    ):
+        self._transport = transport
+        self._tracer = get_tracer(
+            __name__,
+            instrumenting_library_version=__version__,
+            tracer_provider=tracer_provider,
+        )
+        self._request_hook = request_hook
+        self._response_hook = response_hook
+
+    async def handle_async_request(
+        self,
+        method: bytes,
+        url: URL,
+        headers: typing.Optional[Headers] = None,
+        stream: typing.Optional[httpx.AsyncByteStream] = None,
+        extensions: typing.Optional[dict] = None,
+    ) -> typing.Tuple[int, "Headers", httpx.AsyncByteStream, dict]:
+        """Add request info to span."""
+        if context.get_value("suppress_instrumentation"):
+            return await self._transport.handle_async_request(
+                method,
+                url,
+                headers=headers,
+                stream=stream,
+                extensions=extensions,
+            )
+
+        span_attributes = _prepare_attributes(method, url)
+        _headers = _prepare_headers(headers)
+        span_name = _get_default_span_name(
+            span_attributes[SpanAttributes.HTTP_METHOD]
+        )
+        request = RequestInfo(method, url, headers, stream, extensions)
+
+        with self._tracer.start_as_current_span(
+            span_name, kind=SpanKind.CLIENT, attributes=span_attributes
+        ) as span:
+            if self._request_hook is not None:
+                await self._request_hook(span, request)
+
+            inject(_headers)
+
+            (
+                status_code,
+                headers,
+                stream,
+                extensions,
+            ) = await self._transport.handle_async_request(
+                method,
+                url,
+                headers=_headers.raw,
+                stream=stream,
+                extensions=extensions,
+            )
+
+            _apply_status_code(span, status_code)
+
+            if self._response_hook is not None:
+                await self._response_hook(
+                    span,
+                    request,
+                    ResponseInfo(status_code, headers, stream, extensions),
+                )
+
+        return status_code, headers, stream, extensions
+
+
+def _instrument(
+    tracer_provider: TracerProvider = None,
+    request_hook: typing.Optional[RequestHook] = None,
+    response_hook: typing.Optional[ResponseHook] = None,
+) -> None:
+    """Enables tracing of all Client and AsyncClient instances
+
+    When a Client or AsyncClient gets created, a telemetry transport is passed
+    in to the instance.
+    """
+    # pylint:disable=unused-argument
+    def instrumented_sync_send(wrapped, instance, args, kwargs):
+        if context.get_value("suppress_instrumentation"):
+            return wrapped(*args, **kwargs)
+
+        transport = instance._transport or httpx.HTTPTransport()
+        telemetry_transport = SyncOpenTelemetryTransport(
+            transport,
+            tracer_provider=tracer_provider,
+            request_hook=request_hook,
+            response_hook=response_hook,
+        )
+
+        instance._transport = telemetry_transport
+        return wrapped(*args, **kwargs)
+
+    async def instrumented_async_send(wrapped, instance, args, kwargs):
+        if context.get_value("suppress_instrumentation"):
+            return await wrapped(*args, **kwargs)
+
+        transport = instance._transport or httpx.AsyncHTTPTransport()
+        telemetry_transport = AsyncOpenTelemetryTransport(
+            transport,
+            tracer_provider=tracer_provider,
+            request_hook=request_hook,
+            response_hook=response_hook,
+        )
+
+        instance._transport = telemetry_transport
+        return await wrapped(*args, **kwargs)
+
+    wrapt.wrap_function_wrapper(httpx.Client, "send", instrumented_sync_send)
+
+    wrapt.wrap_function_wrapper(
+        httpx.AsyncClient, "send", instrumented_async_send
+    )
+
+
+def _instrument_client(
+    client: typing.Union[httpx.Client, httpx.AsyncClient],
+    tracer_provider: TracerProvider = None,
+    request_hook: typing.Optional[RequestHook] = None,
+    response_hook: typing.Optional[ResponseHook] = None,
+) -> None:
+    """Enables instrumentation for the given Client or AsyncClient"""
+    # pylint: disable=protected-access
+    if isinstance(client, httpx.Client):
+        transport = client._transport or httpx.HTTPTransport()
+        telemetry_transport = SyncOpenTelemetryTransport(
+            transport,
+            tracer_provider=tracer_provider,
+            request_hook=request_hook,
+            response_hook=response_hook,
+        )
+    elif isinstance(client, httpx.AsyncClient):
+        transport = client._transport or httpx.AsyncHTTPTransport()
+        telemetry_transport = AsyncOpenTelemetryTransport(
+            transport,
+            tracer_provider=tracer_provider,
+            request_hook=request_hook,
+            response_hook=response_hook,
+        )
+    else:
+        raise TypeError("Invalid client provided")
+    client._transport = telemetry_transport
+
+
+def _uninstrument() -> None:
+    """Disables instrumenting for all newly created Client and AsyncClient instances"""
+    unwrap(httpx.Client, "send")
+    unwrap(httpx.AsyncClient, "send")
+
+
+def _uninstrument_client(
+    client: typing.Union[httpx.Client, httpx.AsyncClient]
+) -> None:
+    """Disables instrumentation for the given Client or AsyncClient"""
+    # pylint: disable=protected-access
+    unwrap(client, "send")
+
+
+class HTTPXClientInstrumentor(BaseInstrumentor):
+    """An instrumentor for httpx Client and AsyncClient
+
+    See `BaseInstrumentor`
+    """
+
+    def instrumentation_dependencies(self) -> typing.Collection[str]:
+        return _instruments
+
+    def _instrument(self, **kwargs):
+        """Instruments httpx Client and AsyncClient
+
+        Args:
+            **kwargs: Optional arguments
+                ``tracer_provider``: a TracerProvider, defaults to global
+                ``request_hook``: A hook that receives the span and request that is called
+                    right after the span is created
+                ``response_hook``: A hook that receives the span, request, and response
+                    that is called right before the span ends
+        """
+        _instrument(
+            tracer_provider=kwargs.get("tracer_provider"),
+            request_hook=kwargs.get("request_hook"),
+            response_hook=kwargs.get("response_hook"),
+        )
+
+    def _uninstrument(self, **kwargs):
+        _uninstrument()
+
+    @staticmethod
+    def instrument_client(
+        client: typing.Union[httpx.Client, httpx.AsyncClient],
+        tracer_provider: TracerProvider = None,
+        request_hook: typing.Optional[RequestHook] = None,
+        response_hook: typing.Optional[ResponseHook] = None,
+    ) -> None:
+        """Instrument httpx Client or AsyncClient
+
+        Args:
+            client: The httpx Client or AsyncClient instance
+            tracer_provider: A TracerProvider, defaults to global
+            request_hook: A hook that receives the span and request that is called
+                right after the span is created
+            response_hook: A hook that receives the span, request, and response
+                that is called right before the span ends
+        """
+        _instrument_client(
+            client,
+            tracer_provider=tracer_provider,
+            request_hook=request_hook,
+            response_hook=response_hook,
+        )
+
+    @staticmethod
+    def uninstrument_client(
+        client: typing.Union[httpx.Client, httpx.AsyncClient]
+    ):
+        """Disables instrumentation for the given client instance
+
+        Args:
+            client: The httpx Client or AsyncClient instance
+        """
+        _uninstrument_client(client)
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/package.py b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/package.py
new file mode 100644
index 0000000000..08bbe77f9c
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/package.py
@@ -0,0 +1,16 @@
+# Copyright The OpenTelemetry Authors
+#
+# 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.
+
+
+_instruments = ("httpx >= 0.18.0, < 0.19.0",)
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/version.py b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/version.py
new file mode 100644
index 0000000000..c829b95757
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/src/opentelemetry/instrumentation/httpx/version.py
@@ -0,0 +1,15 @@
+# Copyright The OpenTelemetry Authors
+#
+# 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.
+
+__version__ = "0.23.dev0"
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-httpx/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py
new file mode 100644
index 0000000000..e6d8427341
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-httpx/tests/test_httpx_integration.py
@@ -0,0 +1,669 @@
+# Copyright The OpenTelemetry Authors
+#
+# 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.
+
+import abc
+import asyncio
+import typing
+from unittest import mock
+
+import httpx
+import respx
+
+import opentelemetry.instrumentation.httpx
+from opentelemetry import context, trace
+from opentelemetry.instrumentation.httpx import (
+    AsyncOpenTelemetryTransport,
+    HTTPXClientInstrumentor,
+    SyncOpenTelemetryTransport,
+)
+from opentelemetry.propagate import get_global_textmap, set_global_textmap
+from opentelemetry.sdk import resources
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.test.mock_textmap import MockTextMapPropagator
+from opentelemetry.test.test_base import TestBase
+from opentelemetry.trace import StatusCode
+
+if typing.TYPE_CHECKING:
+    from opentelemetry.instrumentation.httpx import (
+        AsyncRequestHook,
+        AsyncResponseHook,
+        RequestHook,
+        RequestInfo,
+        ResponseHook,
+        ResponseInfo,
+    )
+    from opentelemetry.sdk.trace.export import SpanExporter
+    from opentelemetry.trace import TracerProvider
+    from opentelemetry.trace.span import Span
+
+
+HTTP_RESPONSE_BODY = "http.response.body"
+
+
+def _async_call(coro: typing.Coroutine) -> asyncio.Task:
+    loop = asyncio.get_event_loop()
+    return loop.run_until_complete(coro)
+
+
+def _response_hook(span, request: "RequestInfo", response: "ResponseInfo"):
+    span.set_attribute(
+        HTTP_RESPONSE_BODY, response[2].read(),
+    )
+
+
+async def _async_response_hook(
+    span: "Span", request: "RequestInfo", response: "ResponseInfo"
+):
+    span.set_attribute(
+        HTTP_RESPONSE_BODY, await response[2].aread(),
+    )
+
+
+def _request_hook(span: "Span", request: "RequestInfo"):
+    url = httpx.URL(request[1])
+    span.update_name("GET" + str(url))
+
+
+async def _async_request_hook(span: "Span", request: "RequestInfo"):
+    url = httpx.URL(request[1])
+    span.update_name("GET" + str(url))
+
+
+def _no_update_request_hook(span: "Span", request: "RequestInfo"):
+    return 123
+
+
+async def _async_no_update_request_hook(span: "Span", request: "RequestInfo"):
+    return 123
+
+
+# Using this wrapper class to have a base class for the tests while also not
+# angering pylint or mypy when calling methods not in the class when only
+# subclassing abc.ABC.
+class BaseTestCases:
+    class BaseTest(TestBase, metaclass=abc.ABCMeta):
+        # pylint: disable=no-member
+
+        URL = "http://httpbin.org/status/200"
+        response_hook = staticmethod(_response_hook)
+        request_hook = staticmethod(_request_hook)
+        no_update_request_hook = staticmethod(_no_update_request_hook)
+
+        # pylint: disable=invalid-name
+        def setUp(self):
+            super().setUp()
+            respx.start()
+            respx.get(self.URL).mock(httpx.Response(200, text="Hello!"))
+
+        # pylint: disable=invalid-name
+        def tearDown(self):
+            super().tearDown()
+            respx.stop()
+
+        def assert_span(
+            self, exporter: "SpanExporter" = None, num_spans: int = 1
+        ):
+            if exporter is None:
+                exporter = self.memory_exporter
+            span_list = exporter.get_finished_spans()
+            self.assertEqual(num_spans, len(span_list))
+            if num_spans == 0:
+                return None
+            if num_spans == 1:
+                return span_list[0]
+            return span_list
+
+        @abc.abstractmethod
+        def perform_request(
+            self,
+            url: str,
+            method: str = "GET",
+            headers: typing.Dict[str, str] = None,
+            client: typing.Union[httpx.Client, httpx.AsyncClient, None] = None,
+        ):
+            pass
+
+        def test_basic(self):
+            result = self.perform_request(self.URL)
+            self.assertEqual(result.text, "Hello!")
+            span = self.assert_span()
+
+            self.assertIs(span.kind, trace.SpanKind.CLIENT)
+            self.assertEqual(span.name, "HTTP GET")
+
+            self.assertEqual(
+                span.attributes,
+                {
+                    SpanAttributes.HTTP_METHOD: "GET",
+                    SpanAttributes.HTTP_URL: self.URL,
+                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                },
+            )
+
+            self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
+
+            self.check_span_instrumentation_info(
+                span, opentelemetry.instrumentation.httpx
+            )
+
+        def test_not_foundbasic(self):
+            url_404 = "http://httpbin.org/status/404"
+
+            with respx.mock:
+                respx.get(url_404).mock(httpx.Response(404))
+                result = self.perform_request(url_404)
+
+            self.assertEqual(result.status_code, 404)
+            span = self.assert_span()
+            self.assertEqual(
+                span.attributes.get(SpanAttributes.HTTP_STATUS_CODE), 404
+            )
+            self.assertIs(
+                span.status.status_code, trace.StatusCode.ERROR,
+            )
+
+        def test_suppress_instrumentation(self):
+            token = context.attach(
+                context.set_value("suppress_instrumentation", True)
+            )
+            try:
+                result = self.perform_request(self.URL)
+                self.assertEqual(result.text, "Hello!")
+            finally:
+                context.detach(token)
+
+            self.assert_span(num_spans=0)
+
+        def test_distributed_context(self):
+            previous_propagator = get_global_textmap()
+            try:
+                set_global_textmap(MockTextMapPropagator())
+                result = self.perform_request(self.URL)
+                self.assertEqual(result.text, "Hello!")
+
+                span = self.assert_span()
+
+                headers = dict(respx.calls.last.request.headers)
+                self.assertIn(MockTextMapPropagator.TRACE_ID_KEY, headers)
+                self.assertEqual(
+                    str(span.get_span_context().trace_id),
+                    headers[MockTextMapPropagator.TRACE_ID_KEY],
+                )
+                self.assertIn(MockTextMapPropagator.SPAN_ID_KEY, headers)
+                self.assertEqual(
+                    str(span.get_span_context().span_id),
+                    headers[MockTextMapPropagator.SPAN_ID_KEY],
+                )
+
+            finally:
+                set_global_textmap(previous_propagator)
+
+        def test_requests_500_error(self):
+            respx.get(self.URL).mock(httpx.Response(500))
+
+            self.perform_request(self.URL)
+
+            span = self.assert_span()
+            self.assertEqual(
+                span.attributes,
+                {
+                    SpanAttributes.HTTP_METHOD: "GET",
+                    SpanAttributes.HTTP_URL: self.URL,
+                    SpanAttributes.HTTP_STATUS_CODE: 500,
+                },
+            )
+            self.assertEqual(span.status.status_code, StatusCode.ERROR)
+
+        def test_requests_basic_exception(self):
+            with respx.mock, self.assertRaises(Exception):
+                respx.get(self.URL).mock(side_effect=Exception)
+                self.perform_request(self.URL)
+
+            span = self.assert_span()
+            self.assertEqual(span.status.status_code, StatusCode.ERROR)
+
+        def test_requests_timeout_exception(self):
+            with respx.mock, self.assertRaises(httpx.TimeoutException):
+                respx.get(self.URL).mock(side_effect=httpx.TimeoutException)
+                self.perform_request(self.URL)
+
+            span = self.assert_span()
+            self.assertEqual(span.status.status_code, StatusCode.ERROR)
+
+        def test_invalid_url(self):
+            url = "invalid://nope"
+
+            with respx.mock, self.assertRaises(httpx.UnsupportedProtocol):
+                respx.post("invalid://nope").pass_through()
+                self.perform_request(url, method="POST")
+
+            span = self.assert_span()
+
+            self.assertEqual(span.name, "HTTP POST")
+            print(span.attributes)
+            self.assertEqual(
+                span.attributes,
+                {
+                    SpanAttributes.HTTP_METHOD: "POST",
+                    SpanAttributes.HTTP_URL: "invalid://nope/",
+                },
+            )
+            self.assertEqual(span.status.status_code, StatusCode.ERROR)
+
+        def test_if_headers_equals_none(self):
+            result = self.perform_request(self.URL)
+            self.assertEqual(result.text, "Hello!")
+            self.assert_span()
+
+    class BaseManualTest(BaseTest, metaclass=abc.ABCMeta):
+        @abc.abstractmethod
+        def create_transport(
+            self,
+            tracer_provider: typing.Optional["TracerProvider"] = None,
+            request_hook: typing.Optional["RequestHook"] = None,
+            response_hook: typing.Optional["ResponseHook"] = None,
+        ):
+            pass
+
+        @abc.abstractmethod
+        def create_client(
+            self,
+            transport: typing.Union[
+                SyncOpenTelemetryTransport, AsyncOpenTelemetryTransport, None
+            ] = None,
+        ):
+            pass
+
+        def test_default_client(self):
+            client = self.create_client(transport=None)
+            result = self.perform_request(self.URL, client=client)
+            self.assertEqual(result.text, "Hello!")
+            self.assert_span(num_spans=0)
+
+            result = self.perform_request(self.URL)
+            self.assertEqual(result.text, "Hello!")
+            self.assert_span()
+
+        def test_custom_tracer_provider(self):
+            resource = resources.Resource.create({})
+            result = self.create_tracer_provider(resource=resource)
+            tracer_provider, exporter = result
+
+            transport = self.create_transport(tracer_provider=tracer_provider)
+            client = self.create_client(transport)
+            result = self.perform_request(self.URL, client=client)
+
+            self.assertEqual(result.text, "Hello!")
+            span = self.assert_span(exporter=exporter)
+            self.assertIs(span.resource, resource)
+
+        def test_response_hook(self):
+            transport = self.create_transport(
+                tracer_provider=self.tracer_provider,
+                response_hook=self.response_hook,
+            )
+            client = self.create_client(transport)
+            result = self.perform_request(self.URL, client=client)
+
+            self.assertEqual(result.text, "Hello!")
+            span = self.assert_span()
+            self.assertEqual(
+                span.attributes,
+                {
+                    SpanAttributes.HTTP_METHOD: "GET",
+                    SpanAttributes.HTTP_URL: self.URL,
+                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                    HTTP_RESPONSE_BODY: "Hello!",
+                },
+            )
+
+        def test_request_hook(self):
+            transport = self.create_transport(request_hook=self.request_hook)
+            client = self.create_client(transport)
+            result = self.perform_request(self.URL, client=client)
+
+            self.assertEqual(result.text, "Hello!")
+            span = self.assert_span()
+            self.assertEqual(span.name, "GET" + self.URL)
+
+        def test_request_hook_no_span_change(self):
+            transport = self.create_transport(
+                request_hook=self.no_update_request_hook
+            )
+            client = self.create_client(transport)
+            result = self.perform_request(self.URL, client=client)
+
+            self.assertEqual(result.text, "Hello!")
+            span = self.assert_span()
+            self.assertEqual(span.name, "HTTP GET")
+
+        def test_not_recording(self):
+            with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span:
+                transport = self.create_transport(
+                    tracer_provider=trace._DefaultTracerProvider()
+                )
+                client = self.create_client(transport)
+                mock_span.is_recording.return_value = False
+                result = self.perform_request(self.URL, client=client)
+
+                self.assertEqual(result.text, "Hello!")
+                self.assert_span(None, 0)
+                self.assertFalse(mock_span.is_recording())
+                self.assertTrue(mock_span.is_recording.called)
+                self.assertFalse(mock_span.set_attribute.called)
+                self.assertFalse(mock_span.set_status.called)
+
+    class BaseInstrumentorTest(BaseTest, metaclass=abc.ABCMeta):
+        @abc.abstractmethod
+        def create_client(
+            self,
+            transport: typing.Union[
+                SyncOpenTelemetryTransport, AsyncOpenTelemetryTransport, None
+            ] = None,
+        ):
+            pass
+
+        def setUp(self):
+            self.client = self.create_client()
+            HTTPXClientInstrumentor().instrument()
+            super().setUp()
+
+        def tearDown(self):
+            super().tearDown()
+            HTTPXClientInstrumentor().uninstrument()
+
+        def test_custom_tracer_provider(self):
+            resource = resources.Resource.create({})
+            result = self.create_tracer_provider(resource=resource)
+            tracer_provider, exporter = result
+
+            HTTPXClientInstrumentor().uninstrument()
+            HTTPXClientInstrumentor().instrument(
+                tracer_provider=tracer_provider
+            )
+            client = self.create_client()
+            result = self.perform_request(self.URL, client=client)
+
+            self.assertEqual(result.text, "Hello!")
+            span = self.assert_span(exporter=exporter)
+            self.assertIs(span.resource, resource)
+
+        def test_response_hook(self):
+            HTTPXClientInstrumentor().uninstrument()
+            HTTPXClientInstrumentor().instrument(
+                tracer_provider=self.tracer_provider,
+                response_hook=self.response_hook,
+            )
+            client = self.create_client()
+            result = self.perform_request(self.URL, client=client)
+
+            self.assertEqual(result.text, "Hello!")
+            span = self.assert_span()
+            self.assertEqual(
+                span.attributes,
+                {
+                    SpanAttributes.HTTP_METHOD: "GET",
+                    SpanAttributes.HTTP_URL: self.URL,
+                    SpanAttributes.HTTP_STATUS_CODE: 200,
+                    HTTP_RESPONSE_BODY: "Hello!",
+                },
+            )
+
+        def test_request_hook(self):
+            HTTPXClientInstrumentor().uninstrument()
+            HTTPXClientInstrumentor().instrument(
+                tracer_provider=self.tracer_provider,
+                request_hook=self.request_hook,
+            )
+            client = self.create_client()
+            result = self.perform_request(self.URL, client=client)
+
+            self.assertEqual(result.text, "Hello!")
+            span = self.assert_span()
+            self.assertEqual(span.name, "GET" + self.URL)
+
+        def test_request_hook_no_span_update(self):
+            HTTPXClientInstrumentor().uninstrument()
+            HTTPXClientInstrumentor().instrument(
+                tracer_provider=self.tracer_provider,
+                request_hook=self.no_update_request_hook,
+            )
+            client = self.create_client()
+            result = self.perform_request(self.URL, client=client)
+
+            self.assertEqual(result.text, "Hello!")
+            span = self.assert_span()
+            self.assertEqual(span.name, "HTTP GET")
+
+        def test_not_recording(self):
+            with mock.patch("opentelemetry.trace.INVALID_SPAN") as mock_span:
+                HTTPXClientInstrumentor().uninstrument()
+                HTTPXClientInstrumentor().instrument(
+                    tracer_provider=trace._DefaultTracerProvider()
+                )
+                client = self.create_client()
+
+                mock_span.is_recording.return_value = False
+                result = self.perform_request(self.URL, client=client)
+
+                self.assertEqual(result.text, "Hello!")
+                self.assert_span(None, 0)
+                self.assertFalse(mock_span.is_recording())
+                self.assertTrue(mock_span.is_recording.called)
+                self.assertFalse(mock_span.set_attribute.called)
+                self.assertFalse(mock_span.set_status.called)
+
+        def test_suppress_instrumentation_new_client(self):
+            token = context.attach(
+                context.set_value("suppress_instrumentation", True)
+            )
+            try:
+                client = self.create_client()
+                result = self.perform_request(self.URL, client=client)
+                self.assertEqual(result.text, "Hello!")
+            finally:
+                context.detach(token)
+
+            self.assert_span(num_spans=0)
+
+        def test_existing_client(self):
+            HTTPXClientInstrumentor().uninstrument()
+            client = self.create_client()
+            HTTPXClientInstrumentor().instrument()
+            result = self.perform_request(self.URL, client=client)
+            self.assertEqual(result.text, "Hello!")
+            self.assert_span(num_spans=1)
+
+        def test_instrument_client(self):
+            HTTPXClientInstrumentor().uninstrument()
+            client = self.create_client()
+            HTTPXClientInstrumentor().instrument_client(client)
+            result = self.perform_request(self.URL, client=client)
+            self.assertEqual(result.text, "Hello!")
+            self.assert_span(num_spans=1)
+            # instrument again to avoid annoying warning message
+            HTTPXClientInstrumentor().instrument()
+
+        def test_uninstrument(self):
+            HTTPXClientInstrumentor().uninstrument()
+            result = self.perform_request(self.URL)
+            self.assertEqual(result.text, "Hello!")
+            self.assert_span(num_spans=0)
+            # instrument again to avoid annoying warning message
+            HTTPXClientInstrumentor().instrument()
+
+        def test_uninstrument_client(self):
+            HTTPXClientInstrumentor().uninstrument_client(self.client)
+
+            result = self.perform_request(self.URL)
+
+            self.assertEqual(result.text, "Hello!")
+            self.assert_span(num_spans=0)
+
+        def test_uninstrument_new_client(self):
+            client1 = self.create_client()
+            HTTPXClientInstrumentor().uninstrument_client(client1)
+
+            result = self.perform_request(self.URL, client=client1)
+            self.assertEqual(result.text, "Hello!")
+            self.assert_span(num_spans=0)
+
+            # Test that other clients as well as instance client is still
+            # instrumented
+            client2 = self.create_client()
+            result = self.perform_request(self.URL, client=client2)
+            self.assertEqual(result.text, "Hello!")
+            self.assert_span()
+
+            self.memory_exporter.clear()
+
+            result = self.perform_request(self.URL)
+            self.assertEqual(result.text, "Hello!")
+            self.assert_span()
+
+
+class TestSyncIntegration(BaseTestCases.BaseManualTest):
+    def setUp(self):
+        super().setUp()
+        self.transport = self.create_transport()
+        self.client = self.create_client(self.transport)
+
+    def tearDown(self):
+        super().tearDown()
+        self.client.close()
+
+    def create_transport(
+        self,
+        tracer_provider: typing.Optional["TracerProvider"] = None,
+        request_hook: typing.Optional["RequestHook"] = None,
+        response_hook: typing.Optional["ResponseHook"] = None,
+    ):
+        transport = httpx.HTTPTransport()
+        telemetry_transport = SyncOpenTelemetryTransport(
+            transport,
+            tracer_provider=tracer_provider,
+            request_hook=request_hook,
+            response_hook=response_hook,
+        )
+        return telemetry_transport
+
+    def create_client(
+        self, transport: typing.Optional[SyncOpenTelemetryTransport] = None,
+    ):
+        return httpx.Client(transport=transport)
+
+    def perform_request(
+        self,
+        url: str,
+        method: str = "GET",
+        headers: typing.Dict[str, str] = None,
+        client: typing.Union[httpx.Client, httpx.AsyncClient, None] = None,
+    ):
+        if client is None:
+            return self.client.request(method, url, headers=headers)
+        return client.request(method, url, headers=headers)
+
+
+class TestAsyncIntegration(BaseTestCases.BaseManualTest):
+    response_hook = staticmethod(_async_response_hook)
+    request_hook = staticmethod(_async_request_hook)
+    no_update_request_hook = staticmethod(_async_no_update_request_hook)
+
+    def setUp(self):
+        super().setUp()
+        self.transport = self.create_transport()
+        self.client = self.create_client(self.transport)
+
+    def create_transport(
+        self,
+        tracer_provider: typing.Optional["TracerProvider"] = None,
+        request_hook: typing.Optional["AsyncRequestHook"] = None,
+        response_hook: typing.Optional["AsyncResponseHook"] = None,
+    ):
+        transport = httpx.AsyncHTTPTransport()
+        telemetry_transport = AsyncOpenTelemetryTransport(
+            transport,
+            tracer_provider=tracer_provider,
+            request_hook=request_hook,
+            response_hook=response_hook,
+        )
+        return telemetry_transport
+
+    def create_client(
+        self, transport: typing.Optional[AsyncOpenTelemetryTransport] = None,
+    ):
+        return httpx.AsyncClient(transport=transport)
+
+    def perform_request(
+        self,
+        url: str,
+        method: str = "GET",
+        headers: typing.Dict[str, str] = None,
+        client: typing.Union[httpx.Client, httpx.AsyncClient, None] = None,
+    ):
+        async def _perform_request():
+            nonlocal client
+            nonlocal method
+            if client is None:
+                client = self.client
+            async with client as _client:
+                return await _client.request(method, url, headers=headers)
+
+        return _async_call(_perform_request())
+
+
+class TestSyncInstrumentationIntegration(BaseTestCases.BaseInstrumentorTest):
+    def create_client(
+        self, transport: typing.Optional[SyncOpenTelemetryTransport] = None,
+    ):
+        return httpx.Client()
+
+    def perform_request(
+        self,
+        url: str,
+        method: str = "GET",
+        headers: typing.Dict[str, str] = None,
+        client: typing.Union[httpx.Client, httpx.AsyncClient, None] = None,
+    ):
+        if client is None:
+            return self.client.request(method, url, headers=headers)
+        return client.request(method, url, headers=headers)
+
+
+class TestAsyncInstrumentationIntegration(BaseTestCases.BaseInstrumentorTest):
+    response_hook = staticmethod(_async_response_hook)
+    request_hook = staticmethod(_async_request_hook)
+    no_update_request_hook = staticmethod(_async_no_update_request_hook)
+
+    def create_client(
+        self, transport: typing.Optional[AsyncOpenTelemetryTransport] = None,
+    ):
+        return httpx.AsyncClient()
+
+    def perform_request(
+        self,
+        url: str,
+        method: str = "GET",
+        headers: typing.Dict[str, str] = None,
+        client: typing.Union[httpx.Client, httpx.AsyncClient, None] = None,
+    ):
+        async def _perform_request():
+            nonlocal client
+            nonlocal method
+            if client is None:
+                client = self.client
+            async with client as _client:
+                return await _client.request(method, url, headers=headers)
+
+        return _async_call(_perform_request())
diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
index a7d4d54000..e5a3293c3b 100644
--- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
+++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py
@@ -68,6 +68,10 @@
         "library": "grpcio ~= 1.27",
         "instrumentation": "opentelemetry-instrumentation-grpc==0.23.dev0",
     },
+    "httpx": {
+        "library": "httpx >= 0.18.0, < 0.19.0",
+        "instrumentation": "opentelemetry-instrumentation-httpx==0.23.dev0",
+    },
     "jinja2": {
         "library": "jinja2~=2.7",
         "instrumentation": "opentelemetry-instrumentation-jinja2==0.23.dev0",
diff --git a/tox.ini b/tox.ini
index bf3f289f91..72778e3ca1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -144,6 +144,10 @@ envlist =
     py3{6,7,8,9}-test-instrumentation-tornado
     pypy3-test-instrumentation-tornado
 
+    ; opentelemetry-instrumentation-httpx
+    py3{6,7,8,9}-test-instrumentation-httpx
+    pypy3-test-instrumentation-httpx
+
     ; opentelemetry-util-http
     py3{6,7,8,9}-test-util-http
     pypy3-test-util-http
@@ -211,6 +215,7 @@ changedir =
   test-instrumentation-starlette: instrumentation/opentelemetry-instrumentation-starlette/tests
   test-instrumentation-tornado: instrumentation/opentelemetry-instrumentation-tornado/tests
   test-instrumentation-wsgi: instrumentation/opentelemetry-instrumentation-wsgi/tests
+  test-instrumentation-httpx: instrumentation/opentelemetry-instrumentation-httpx/tests
   test-util-http: util/opentelemetry-util-http/tests
   test-sdkextension-aws: sdk-extension/opentelemetry-sdk-extension-aws/tests
   test-propagator-ot-trace: propagator/opentelemetry-propagator-ot-trace/tests
@@ -295,6 +300,8 @@ commands_pre =
 
   elasticsearch{2,5,6,7}: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-elasticsearch[test]
 
+  httpx: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx[test]
+
   aws: pip install requests {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws[test]
 
   http: pip install {toxinidir}/util/opentelemetry-util-http[test]
@@ -381,6 +388,7 @@ commands_pre =
   python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg[test]
   python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-tornado[test]
   python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-mysql[test]
+  python -m pip install -e {toxinidir}/instrumentation/opentelemetry-instrumentation-httpx[test]
   python -m pip install -e {toxinidir}/exporter/opentelemetry-exporter-datadog[test]
   python -m pip install -e {toxinidir}/sdk-extension/opentelemetry-sdk-extension-aws[test]
   python -m pip install -e {toxinidir}/propagator/opentelemetry-propagator-ot-trace[test]