diff --git a/.changes/next-release/36408038949-bugfix-local-5121.json b/.changes/next-release/36408038949-bugfix-local-5121.json
new file mode 100644
index 0000000..7861edf
--- /dev/null
+++ b/.changes/next-release/36408038949-bugfix-local-5121.json
@@ -0,0 +1,5 @@
+{
+  "type": "bugfix",
+  "category": "local",
+  "description": "Make ``current_request`` thread safe in local mode (#759)"
+}
diff --git a/chalice/local.py b/chalice/local.py
index 3be170f..7e37d89 100644
--- a/chalice/local.py
+++ b/chalice/local.py
@@ -25,7 +25,6 @@ from typing import (
     Callable,
     Optional,
     Union,
-    cast,
 )  # noqa
 
 from chalice.app import Chalice  # noqa
@@ -58,9 +57,8 @@ class Clock(object):
 
 def create_local_server(app_obj, config, host, port):
     # type: (Chalice, Config, str, int) -> LocalDevServer
-    local_app_obj = LocalChalice(app_obj)
-    casted_local_app_obj = cast(Chalice, local_app_obj)
-    return LocalDevServer(casted_local_app_obj, config, host, port)
+    app_obj.__class__ = LocalChalice
+    return LocalDevServer(app_obj, config, host, port)
 
 
 class LocalARNBuilder(object):
@@ -708,30 +706,21 @@ class HTTPServerThread(threading.Thread):
             self._server.shutdown()
 
 
-class LocalChalice(object):
-    def __init__(self, chalice):
-        # type: (Chalice) -> None
-        self._current_request_lookup = {}  # type: Dict[int, Optional[Request]]
-        self._chalice = chalice
+class LocalChalice(Chalice):
+
+    _THREAD_LOCAL = threading.local()
+
+    # This is a known mypy bug where you can't override instance
+    # variables with properties.  So this should be type safe, which
+    # is why we're adding the type: ignore comments here.
+    # See: https://github.com/python/mypy/issues/4125
 
-    @property
-    def current_request(self):  # noqa
-        # type: () -> Optional[Request]
-        thread_id = threading.current_thread().ident
-        assert thread_id is not None
-        return self._current_request_lookup.get(thread_id, None)
+    @property  # type: ignore
+    def current_request(self):  # type: ignore
+        # type: () -> Request
+        return self._THREAD_LOCAL.current_request
 
     @current_request.setter
-    def current_request(self, value):  # noqa
-        # type: (Optional[Request]) -> None
-        thread_id = threading.current_thread().ident
-        assert thread_id is not None
-        self._current_request_lookup[thread_id] = value
-
-    def __getattr__(self, name):
-        # type: (str) -> Any
-        return getattr(self._chalice, name)
-
-    def __call__(self, *args, **kwargs):
-        # type: (Any, Any) -> Any
-        return self._chalice(*args, **kwargs)
+    def current_request(self, value):  # type: ignore
+        # type: (Request) -> None
+        self._THREAD_LOCAL.current_request = value

app.current_request is not thread safe
I'm running into a problem when testing using chalice local and making calls to my service at almost the same time. It seems like app.current_request could get overwritten by a later request by the time I finish handling an earlier one so my current handler is working with a request object with context for the later request (i.e. I'm handling Request 1, Request 2 comes in, Request 3 comes in, my handler for request 2 uses app.current_request and gets the context for request 3). 

Would it be better to pass the request object in to the route?

Is this a potential problem when running in lambda or does each handler get a separate process?
diff --git a/tests/functional/test_local.py b/tests/functional/test_local.py
index a66128b..7c4115c 100644
--- a/tests/functional/test_local.py
+++ b/tests/functional/test_local.py
@@ -4,6 +4,7 @@ import time
 import contextlib
 from threading import Thread
 from threading import Event
+from threading import Lock
 import json
 import subprocess
 from contextlib import contextmanager
@@ -14,7 +15,7 @@ from urllib3.util.retry import Retry
 from requests.adapters import HTTPAdapter
 
 from chalice import app
-from chalice.local import LocalDevServer
+from chalice.local import create_local_server
 from chalice.config import Config
 from chalice.utils import OSUtils
 
@@ -71,7 +72,7 @@ class ThreadedLocalServer(Thread):
         self._config = config
 
     def run(self):
-        self._server = LocalDevServer(
+        self._server = create_local_server(
             self._app_object, self._config, self._host, self._port)
         self._server_ready.set()
         self._server.serve_forever()
@@ -103,10 +104,10 @@ def http_session():
     session = requests.Session()
     retry = Retry(
         # How many connection-related errors to retry on.
-        connect=5,
-        # How many connection-related errors to retry on.
+        connect=10,
         # A backoff factor to apply between attempts after the second try.
         backoff_factor=2,
+        method_whitelist=['GET', 'POST', 'PUT'],
     )
     session.mount('http://', HTTPAdapter(max_retries=retry))
     return HTTPFetcher(session)
@@ -141,6 +142,9 @@ def local_server_factory(unused_tcp_port):
 def sample_app():
     demo = app.Chalice('demo-app')
 
+    thread_safety_check = []
+    lock = Lock()
+
     @demo.route('/', methods=['GET'])
     def index():
         return {'hello': 'world'}
@@ -149,9 +153,56 @@ def sample_app():
     def test_cors():
         return {'hello': 'world'}
 
+    @demo.route('/count', methods=['POST'])
+    def record_counter():
+        # An extra delay helps ensure we consistently fail if we're
+        # not thread safe.
+        time.sleep(0.001)
+        count = int(demo.current_request.json_body['counter'])
+        with lock:
+            thread_safety_check.append(count)
+
+    @demo.route('/count', methods=['GET'])
+    def get_record_counter():
+        return thread_safety_check[:]
+
     return demo
 
 
+def test_has_thread_safe_current_request(config, sample_app,
+                                         local_server_factory):
+    local_server, port = local_server_factory(sample_app, config)
+    local_server.wait_for_server_ready()
+
+    num_requests = 25
+    num_threads = 5
+
+    # The idea here is that each requests.post() has a unique 'counter'
+    # integer.  If the current request is thread safe we should see a number
+    # for each 0 - (num_requests * num_threads).  If it's not thread safe
+    # we'll see missing numbers and/or duplicates.
+    def make_requests(counter_start):
+        for i in range(counter_start * num_requests,
+                       (counter_start + 1) * num_requests):
+            # We're slowing the sending rate down a bit.  The threaded
+            # http server is good, but not great.  You can still overwhelm
+            # it pretty easily.
+            time.sleep(0.001)
+            requests.post(
+                'http://localhost:%s/count' % port, json={'counter': i})
+
+    threads = []
+    for i in range(num_threads):
+        threads.append(Thread(target=make_requests, args=(i,)))
+    for thread in threads:
+        thread.start()
+    for thread in threads:
+        thread.join()
+    response = requests.get('http://localhost:%s/count' % port)
+    assert len(response.json()) == len(range(num_requests * num_threads))
+    assert sorted(response.json()) == list(range(num_requests * num_threads))
+
+
 def test_can_accept_get_request(config, sample_app, local_server_factory):
     local_server, port = local_server_factory(sample_app, config)
     response = local_server.make_call(requests.get, '/', port)
diff --git a/tests/unit/test_local.py b/tests/unit/test_local.py
index 926049d..3ae0966 100644
--- a/tests/unit/test_local.py
+++ b/tests/unit/test_local.py
@@ -21,7 +21,6 @@ from chalice.local import NotAuthorizedError
 from chalice.local import ForbiddenError
 from chalice.local import InvalidAuthorizerError
 from chalice.local import LocalDevServer
-from chalice.local import LocalChalice
 
 
 AWS_REQUEST_ID_PATTERN = re.compile(
@@ -691,21 +690,6 @@ def test_can_provide_host_to_local_server(sample_app):
     assert dev_server.host == '0.0.0.0'
 
 
-def test_wraps_sample_app_with_local_chalice(sample_app):
-    dev_server = local.create_local_server(
-        sample_app, None, "127.0.0.1", 23456
-    )
-    assert isinstance(dev_server.app_object, LocalChalice)
-    assert dev_server.app_object._chalice is sample_app
-    assert dev_server.app_object.app_name is sample_app.app_name
-    dev_server.app_object.current_request = "foo"
-    assert dev_server.app_object.current_request == "foo"
-    assert (
-        dev_server.app_object.current_request
-        is not sample_app.current_request
-    )
-
-
 class TestLambdaContext(object):
     def test_can_get_remaining_time_once(self, lambda_context_args):
         time_source = FakeTimeSource([0, 5])

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio
apt-get update apt-get install -y gcc
3.9
requirements-dev.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
astroid==2.3.3
attrs==19.3.0
boto3==1.9.188
botocore==1.12.253
certifi==2025.1.31
-e git+https://github.com/aws/chalice.git@1378d8b0759034d349f0ef104842da272bd3dbf3#egg=chalice
chardet==3.0.4
click==7.1.2
coverage==4.5.4
distlib==0.3.9
doc8==0.8.0
docutils==0.15.2
entrypoints==0.3
enum-compat==0.0.3
exceptiongroup==1.2.2
execnet==2.1.1
filelock==3.18.0
flake8==3.7.9
hypothesis==4.43.1
idna==2.7
importlib-metadata==8.6.1
iniconfig==2.1.0
isort==4.3.21
jmespath==0.10.0
lazy-object-proxy==1.4.3
mccabe==0.6.1
mock==2.0.0
more-itertools==10.6.0
mypy==0.740
mypy-extensions==0.4.4
packaging==24.2
pbr==6.1.1
platformdirs==4.3.7
pluggy==1.5.0
py==1.5.3
pycodestyle==2.5.0
pydocstyle==2.0.0
pyflakes==2.1.1
Pygments==2.1.3
pylint==2.4.4
pytest==8.3.5
pytest-asyncio==0.26.0
pytest-cov==2.8.1
pytest-mock==3.14.0
pytest-xdist==3.6.1
python-dateutil==2.9.0.post0
requests==2.20.0
restructuredtext-lint==1.4.0
s3transfer==0.2.1
six==1.17.0
snowballstemmer==2.2.0
stevedore==5.4.1
tomli==2.2.1
tox==3.0.0
typed-ast==1.4.3
typing-extensions==4.13.0
urllib3==1.24.3
virtualenv==20.29.3
wcwidth==0.2.13
websocket-client==0.54.0
wrapt==1.11.2
zipp==3.21.0

name: chalice
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - astroid==2.3.3
      - attrs==19.3.0
      - boto3==1.9.188
      - botocore==1.12.253
      - certifi==2025.1.31
      - chardet==3.0.4
      - click==7.1.2
      - coverage==4.5.4
      - distlib==0.3.9
      - doc8==0.8.0
      - docutils==0.15.2
      - entrypoints==0.3
      - enum-compat==0.0.3
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - filelock==3.18.0
      - flake8==3.7.9
      - hypothesis==4.43.1
      - idna==2.7
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - isort==4.3.21
      - jmespath==0.10.0
      - lazy-object-proxy==1.4.3
      - mccabe==0.6.1
      - mock==2.0.0
      - more-itertools==10.6.0
      - mypy==0.740
      - mypy-extensions==0.4.4
      - packaging==24.2
      - pbr==6.1.1
      - pip==20.0.2
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - py==1.5.3
      - pycodestyle==2.5.0
      - pydocstyle==2.0.0
      - pyflakes==2.1.1
      - pygments==2.1.3
      - pylint==2.4.4
      - pytest==8.3.5
      - pytest-asyncio==0.26.0
      - pytest-cov==2.8.1
      - pytest-mock==3.14.0
      - pytest-xdist==3.6.1
      - python-dateutil==2.9.0.post0
      - requests==2.20.0
      - restructuredtext-lint==1.4.0
      - s3transfer==0.2.1
      - six==1.17.0
      - snowballstemmer==2.2.0
      - stevedore==5.4.1
      - tomli==2.2.1
      - tox==3.0.0
      - typed-ast==1.4.3
      - typing-extensions==4.13.0
      - urllib3==1.24.3
      - virtualenv==20.29.3
      - wcwidth==0.2.13
      - websocket-client==0.54.0
      - wheel==0.26.0
      - wrapt==1.11.2
      - zipp==3.21.0
prefix: /opt/conda/envs/chalice

tests/functional/test_local.py::test_has_thread_safe_current_request
tests/unit/test_local.py::TestLocalBuiltinAuthorizers::test_does_authorize_unsupported_authorizer
tests/functional/test_local.py::test_can_accept_get_request tests/functional/test_local.py::test_can_get_unicode_string_content_length tests/functional/test_local.py::test_can_accept_options_request tests/functional/test_local.py::test_can_accept_multiple_options_request tests/functional/test_local.py::test_can_accept_multiple_connections tests/functional/test_local.py::test_can_import_env_vars tests/functional/test_local.py::test_can_reload_server tests/unit/test_local.py::test_can_convert_request_handler_to_lambda_event tests/unit/test_local.py::test_uses_http_11 tests/unit/test_local.py::test_can_route_url_params tests/unit/test_local.py::test_can_route_put_with_body tests/unit/test_local.py::test_will_respond_with_cors_enabled tests/unit/test_local.py::test_will_respond_with_custom_cors_enabled tests/unit/test_local.py::test_will_respond_with_custom_cors_enabled_options tests/unit/test_local.py::test_can_preflight_request tests/unit/test_local.py::test_non_preflight_options_request tests/unit/test_local.py::test_errors_converted_to_json_response tests/unit/test_local.py::test_can_support_delete_method tests/unit/test_local.py::test_can_support_patch_method tests/unit/test_local.py::test_can_support_decimals tests/unit/test_local.py::test_unsupported_methods_raise_error tests/unit/test_local.py::test_can_round_trip_binary tests/unit/test_local.py::test_querystring_is_mapped tests/unit/test_local.py::test_empty_querystring_is_none tests/unit/test_local.py::test_querystring_list_is_mapped tests/unit/test_local.py::test_querystring_undefined_is_mapped_consistent_with_apigateway tests/unit/test_local.py::test_content_type_included_once tests/unit/test_local.py::test_can_deny_unauthed_request tests/unit/test_local.py::test_multi_value_header tests/unit/test_local.py::test_can_match_exact_route[/foo-/foo] tests/unit/test_local.py::test_can_match_exact_route[/foo/-/foo] tests/unit/test_local.py::test_can_match_exact_route[/foo/bar-/foo/bar] tests/unit/test_local.py::test_can_match_exact_route[/foo/other-/foo/{capture}] tests/unit/test_local.py::test_can_match_exact_route[/names/foo-/names/{capture}] tests/unit/test_local.py::test_can_match_exact_route[/names/bar-/names/{capture}] tests/unit/test_local.py::test_can_match_exact_route[/names/bar/-/names/{capture}] tests/unit/test_local.py::test_can_match_exact_route[/names/-None] tests/unit/test_local.py::test_can_match_exact_route[/nomatch-None] tests/unit/test_local.py::test_can_match_exact_route[/names/bar/wrong-None] tests/unit/test_local.py::test_can_match_exact_route[/a/z/c-/a/{capture}/c] tests/unit/test_local.py::test_can_match_exact_route[/a/b/c-/a/b/c] tests/unit/test_local.py::test_lambda_event_contains_source_ip tests/unit/test_local.py::test_can_create_lambda_event tests/unit/test_local.py::test_parse_query_string tests/unit/test_local.py::test_can_create_lambda_event_for_put_request tests/unit/test_local.py::test_can_create_lambda_event_for_post_with_formencoded_body tests/unit/test_local.py::test_can_provide_port_to_local_server tests/unit/test_local.py::test_can_provide_host_to_local_server tests/unit/test_local.py::TestLambdaContext::test_can_get_remaining_time_once tests/unit/test_local.py::TestLambdaContext::test_can_get_remaining_time_multiple tests/unit/test_local.py::TestLambdaContext::test_does_populate_aws_request_id_with_valid_uuid tests/unit/test_local.py::TestLambdaContext::test_does_set_version_to_latest tests/unit/test_local.py::TestLocalGateway::test_can_invoke_function tests/unit/test_local.py::TestLocalGateway::test_does_populate_context tests/unit/test_local.py::TestLocalGateway::test_can_validate_route_with_variables tests/unit/test_local.py::TestLocalGateway::test_can_allow_route_with_variables tests/unit/test_local.py::TestLocalGateway::test_does_send_500_when_authorizer_returns_none tests/unit/test_local.py::TestLocalGateway::test_can_deny_route_with_variables tests/unit/test_local.py::TestLocalGateway::test_does_deny_unauthed_request tests/unit/test_local.py::TestLocalGateway::test_does_throw_unauthorized_when_no_auth_token_present_on_valid_route tests/unit/test_local.py::TestLocalGateway::test_does_deny_with_forbidden_when_route_not_found tests/unit/test_local.py::TestLocalGateway::test_does_deny_with_forbidden_when_auth_token_present tests/unit/test_local.py::TestLocalBuiltinAuthorizers::test_can_authorize_empty_path tests/unit/test_local.py::TestLocalBuiltinAuthorizers::test_can_call_method_without_auth tests/unit/test_local.py::TestLocalBuiltinAuthorizers::test_does_raise_not_authorized_error tests/unit/test_local.py::TestLocalBuiltinAuthorizers::test_does_authorize_valid_requests tests/unit/test_local.py::TestLocalBuiltinAuthorizers::test_cannot_access_view_without_permission tests/unit/test_local.py::TestLocalBuiltinAuthorizers::test_can_understand_explicit_auth_policy tests/unit/test_local.py::TestLocalBuiltinAuthorizers::test_can_understand_explicit_deny_policy tests/unit/test_local.py::TestLocalBuiltinAuthorizers::test_can_understand_cognito_token tests/unit/test_local.py::TestArnBuilder::test_can_create_basic_arn tests/unit/test_local.py::TestArnBuilder::test_can_create_root_arn tests/unit/test_local.py::TestArnBuilder::test_can_create_multi_part_arn tests/unit/test_local.py::TestArnBuilder::test_can_create_glob_method_arn tests/unit/test_local.py::test_can_allow_route_arns[mars-west-2:123456789012:ymy8tbxw7b/api/GET/foo-mars-west-2:123456789012:ymy8tbxw7b/api/GET/foo] tests/unit/test_local.py::test_can_allow_route_arns[mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar-mars-west-1:123456789012:ymy8tbxw7b/api/GET/*] tests/unit/test_local.py::test_can_allow_route_arns[mars-west-1:123456789012:ymy8tbxw7b/api/PUT/foobar-mars-west-1:123456789012:ymy8tbxw7b/api/???/foobar] tests/unit/test_local.py::test_can_allow_route_arns[mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar-mars-west-1:123456789012:ymy8tbxw7b/api/???/*] tests/unit/test_local.py::test_can_allow_route_arns[mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar-mars-west-1:123456789012:*/api/GET/*] tests/unit/test_local.py::test_can_allow_route_arns[mars-west-2:123456789012:ymy8tbxw7b/api/GET/foobar-*] tests/unit/test_local.py::test_can_allow_route_arns[mars-west-2:123456789012:ymy8tbxw7b/api/GET/foo.bar-mars-west-2:123456789012:ymy8tbxw7b/*/GET/*] tests/unit/test_local.py::test_can_deny_route_arns[mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar-mars-west-1:123456789012:ymy8tbxw7b/api/PUT/*] tests/unit/test_local.py::test_can_deny_route_arns[mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar-mars-west-1:123456789012:ymy8tbxw7b/api/??/foobar] tests/unit/test_local.py::test_can_deny_route_arns[mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar-mars-west-2:123456789012:ymy8tbxw7b/api/???/*] tests/unit/test_local.py::test_can_deny_route_arns[mars-west-2:123456789012:ymy8tbxw7b/api/GET/foobar-mars-west-2:123456789012:ymy8tbxw7b/*/GET/foo...] tests/unit/test_local.py::test_can_allow_multiple_resource_arns[mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar-patterns0] tests/unit/test_local.py::test_can_allow_multiple_resource_arns[mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar-patterns1] tests/unit/test_local.py::test_can_allow_multiple_resource_arns[mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar-patterns2] tests/unit/test_local.py::test_can_deny_multiple_resource_arns[mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar-patterns0] tests/unit/test_local.py::test_can_deny_multiple_resource_arns[mars-west-1:123456789012:ymy8tbxw7b/api/GET/foobar-patterns1] tests/unit/test_local.py::TestLocalDevServer::test_can_delegate_to_server tests/unit/test_local.py::TestLocalDevServer::test_host_and_port_forwarded_to_server_creation tests/unit/test_local.py::TestLocalDevServer::test_does_use_daemon_threads
chalice/local.py .changes/next-release/36408038949-bugfix-local-5121.json
chalice/local.py .changes/next-release/36408038949-bugfix-local-5121.json