Repository: abhinavsingh/proxy.py

Issue:
[Acceptors] Decouple Threadless pool setup/teardown from acceptor pool
**Is your feature request related to a problem? Please describe.**
Currently, `--num-workers` is shared by both `acceptors` and `threadless` process pool.  In a real world scenario, users may need to configure different number of acceptors vs threadless processes.  Currently, there is a threadless process dedicated for each acceptor process.

**Describe the solution you'd like**
1. Add a `--num-acceptors` flag
2. Decouple threadless pool setup/teardown from acceptor pool class

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
This might be necessary if you are not expecting too many concurrent client requests but each request is a heavy request (e.g. file upload / file download).


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) Report
> Merging [#714](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (3b38f2e) into [develop](https://codecov.io/gh/abhinavsingh/proxy.py/commit/fec9a14303e2394b32abb773f23f515fbb5038bf?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (fec9a14) will **decrease** coverage by `0.18%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714/graphs/tree.svg?width=650&height=150&src=pr&token=Zh9J7b4la2&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)

```diff
@@             Coverage Diff             @@
##           develop     #714      +/-   ##
===========================================
- Coverage    86.16%   85.98%   -0.19%     
===========================================
  Files          113      113              
  Lines         4966     4966              
  Branches       405      405              
===========================================
- Hits          4279     4270       -9     
- Misses         611      615       +4     
- Partials        76       81       +5     
```

| Flag | Coverage Δ | |
|---|---|---|
| pytest | `85.92% <100.00%> (-0.09%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) | Coverage Δ | |
|---|---|---|
| [proxy/core/acceptor/pool.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29yZS9hY2NlcHRvci9wb29sLnB5) | `86.81% <ø> (ø)` | |
| [proxy/proxy.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvcHJveHkucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/test\_set\_open\_file\_limit.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvdGVzdF9zZXRfb3Blbl9maWxlX2xpbWl0LnB5) | `96.29% <0.00%> (-3.71%)` | :arrow_down: |
| [tests/testing/test\_embed.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvdGVzdGluZy90ZXN0X2VtYmVkLnB5) | `91.17% <0.00%> (-2.95%)` | :arrow_down: |
| [proxy/common/constants.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29tbW9uL2NvbnN0YW50cy5weQ==) | `97.59% <0.00%> (-2.41%)` | :arrow_down: |
| [proxy/common/flag.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29tbW9uL2ZsYWcucHk=) | `93.22% <0.00%> (-1.70%)` | :arrow_down: |
| [proxy/common/utils.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29tbW9uL3V0aWxzLnB5) | `91.86% <0.00%> (-1.63%)` | :arrow_down: |
| [tests/test\_main.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvdGVzdF9tYWluLnB5) | `99.40% <0.00%> (-0.60%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh). Last update [fec9a14...3b38f2e](https://codecov.io/gh/abhinavsingh/proxy.py/pull/714?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh).

webknjaz: @abhinavsingh this branch you have flooded the GHA backlog: https://github.com/abhinavsingh/proxy.py/actions?query=is%3Aqueued.
This is because your PR branch lives in the upstream repository.
So each time you push to this branch, there are two events that fire in this repo: `push` and `pull_request`. Each of them starts a workflow.

One of the solutions I've seen in the wild is to forbid creating branches in the upstream repo (we actually have a bot that tries to prevent this at work, in ansible/ansible). And then, the feature branches (except for `devel` and release branches) should exist in the forks of people (even for the maintainers).
Not only this helps with the CI, but it also prevents flooding the contributors' forks with garbage branches. Whenever somebody forks a repo, they get a copy of all the branches at that point in time. The upstream branches get updated/removed but the forks don't get those updates and they remain polluted with unimportant branches. Also, since the forks happen at different points in the repo history, different users/forks would get those branches in different states which may be confusing, especially for Git beginners. This would also spread the load and allow the contributors to get some CI results in their forks before creating a PR even. This is because their pushes to the feature branch will create a `push` event in their fork (never affecting the upstream CI in any way) and the upstream PRs would generate `pull_request` events which is fine even with many updates since there's auto-cancelation.

Action items:
1) Cancel the stale workflows, they get auto-canceled for PRs but not for the regular in-repo pushes.
2) Consider maybe moving the repo into a dedicated org and changing your workflow a little bit.
abhinavsingh: > this branch you have flooded the GHA backlog: https://github.com/abhinavsingh/proxy.py/actions?query=is%3Aqueued.

Lollll, yes, totally.  Apologies for that.  Last night I was onto something, I knew several tests would break, some may even hang, and I didn't cared to fix the tests until I could get it working right.  Out of habit, I kept pushing code upstream too, resulting into hung workflows.

Indeed I did notice duplicate workflows being triggered, one each for `push` and `pull_request`.  I think, one option is to disable workflows for `push` (w/o an open PR).  Because currently, even if I don't create a PR, workflows are being triggered for every push.  Wdyt?  Will it also impact workflows for pushes into an active PR.

> One of the solutions I've seen in the wild is to forbid creating branches in the upstream repo (we actually have a bot that tries to prevent this at work, in ansible/ansible). And then, the feature branches (except for `devel` and release branches) should exist in the forks of people (even for the maintainers).

I see what you mean here.  IIUC, TL;DR is that development should happen in forks which will avoid running workflows within the main repo context.  Though, I think duplicate workflows will still get triggered for every push into personal forks.

I am thinking, may be right solution is to disable `push` only workflows until there is an associated PR for that branch.  This will avoid running unintended workflows both for the upstream repo and personal forked repos.  Of-course disabling on `push` will prohibit developers from seeing CI results until they make a PR.

To make sure we are on the same ground, here is my understanding of how workflows are getting triggered:

1. Configured for `push` and `pull_request` actions
2. Triggered for every `push` (w/o a PR)
3. Triggered when a `pull_request` is created
4. Triggered when there is a `push` into an active PR.  This `push` then in turn also triggers (duplicate) corresponding `pull_request` workflows.

Please correct me if wrong.  I would like to tackle `Item-4` where duplicate workflows are getting triggered.

> Not only this helps with the CI, but it also prevents flooding the contributors' forks with garbage branches. Whenever somebody forks a repo, they get a copy of all the branches at that point in time. The upstream branches get updated/removed but the forks don't get those updates and they remain polluted with unimportant branches.

You have a valid point here.

> This would also spread the load and allow the contributors to get some CI results in their forks before creating a PR even.

Makes sense, we expect forks to have a valid workflows setup.

> 1. Cancel the stale workflows, they get auto-canceled for PRs but not for the regular in-repo pushes.

Done

> 2\. Consider maybe moving the repo into a dedicated org and changing your workflow a little bit.

Totally, soon we'll put `proxy.py` under @jaxl-innovations-private-limited organization.  I have done repo moves under org in the past, but still wondering, any side-effects that we must be concerned about before making this move?

One action item would be to update all `abhinavsingh/` references to `jaxl-innovations-private-limited/` in the codebase artifacts.


webknjaz: > > this branch you have flooded the GHA backlog: [abhinavsingh/proxy.py/actions?query=is%3Aqueued](https://github.com/abhinavsingh/proxy.py/actions?query=is%3Aqueued).
> 
> Lollll, yes, totally. Apologies for that. Last night I was onto something, I knew several tests would break, some may even hang, and I didn't cared to fix the tests until I could get it working right. Out of habit, I kept pushing code upstream too, resulting into hung workflows.

I don't see pushing to the remote as inherently problematic. The only problem is that you don't have a separate fork with separate rate limits in this case so you end up influencing others.

> Indeed I did notice duplicate workflows being triggered, one each for `push` and `pull_request`. I think, one option is to disable workflows for `push` (w/o an open PR). Because currently, even if I don't create a PR, workflows are being triggered for every push. Wdyt? Will it also impact workflows for pushes into an active PR.

I don't like this because it'll also prevent feature branches from being tested in forks when there's no PR.

> > One of the solutions I've seen in the wild is to forbid creating branches in the upstream repo (we actually have a bot that tries to prevent this at work, in ansible/ansible). And then, the feature branches (except for `devel` and release branches) should exist in the forks of people (even for the maintainers).
> 
> I see what you mean here. IIUC, TL;DR is that development should happen in forks which will avoid running workflows within the main repo context. Though, I think duplicate workflows will still get triggered for every push into personal forks.

Not exactly. For forks, the `push` event within the GitHub platform will happen in the context of those forks. When there's a PR to the upstream, from a fork, on PR branch updates the fork will get `push` events and the upstream will get `pull_request` events, `push` will never trigger workflow runs in upstream in this case (but it will trigger those in the corresponding fork repo; the workflows in the fork will not show up among checks in PRs or anywhere in upstream for that matter).

> I am thinking, may be right solution is to disable `push` only workflows until there is an associated PR for that branch. This will avoid running unintended workflows both for the upstream repo and personal forked repos. Of-course disabling on `push` will prohibit developers from seeing CI results until they make a PR.

This is exactly what I dislike. I sometimes push to my fork to see the results before making a PR, with this change, you'd steal this ability and force posting a PR that may not yet be ready (not even for Draft).

> To make sure we are on the same ground, here is my understanding of how workflows are getting triggered:
> 
> 1. Configured for `push` and `pull_request` actions
> 2. Triggered for every `push` (w/o a PR)
> 3. Triggered when a `pull_request` is created
> 4. Triggered when there is a `push` into an active PR.  This `push` then in turn also triggers (duplicate) corresponding `pull_request` workflows.

The `push` **event** doesn't trigger the `pull_request` event. They appear independently as a result of `git push`. The duplication is only visible when a given PR is coming from the same repository and is not a concern in other cases.

> > This would also spread the load and allow the contributors to get some CI results in their forks before creating a PR even.
> 
> Makes sense, we expect forks to have a valid workflows setup.

With your proposal to disable the `push` trigger, you'd break this.

> > 2. Consider maybe moving the repo into a dedicated org and changing your workflow a little bit.
> 
> Totally, soon we'll put `proxy.py` under @jaxl-innovations-private-limited organization.

Is this org tightly bound to proxy.py? If not, maybe create a separate one?

> I have done repo moves under org in the past, but still wondering, any side-effects that we must be concerned about before making this move?

No, GH will create redirects. Both on HTTP level and also for Git too.

> One action item would be to update all `abhinavsingh/` references to `jaxl-innovations-private-limited/` in the codebase artifacts.

This can be done later since the links won't be broken due to redirects.

Patch:
diff --git a/README.md b/README.md
index 4d63bd8..4d8beeb 100644
--- a/README.md
+++ b/README.md
@@ -265,9 +265,10 @@ Simply type `proxy` on command line to start with default configuration.
 
 ```console
 ❯ proxy
-...[redacted]... - Loaded plugin proxy.http_proxy.HttpProxyPlugin
-...[redacted]... - Starting 8 threadless workers
-...[redacted]... - Started server on ::1:8899
+...[redacted]... - Loaded plugin proxy.http.proxy.HttpProxyPlugin
+...[redacted]... - Started 8 threadless workers
+...[redacted]... - Started 8 acceptors
+...[redacted]... - Listening on 127.0.0.1:8899
 ```
 
 ### Understanding logs
@@ -279,10 +280,15 @@ Things to notice from above logs:
   - As name suggests, this core plugin adds `http(s)` proxy server capabilities to `proxy.py` instance
 
 - `Started N threadless workers`
-  - By default, `proxy.py` will start as many workers as there are CPU cores on the machine
+  - By default, `proxy.py` will start as many worker processes as there are CPU cores on the machine
   - Use `--num-workers` flag to customize number of worker processes
   - See [Threads vs Threadless](#threads-vs-threadless) to understand how to control execution mode
 
+- `Started N acceptors`
+  - By default, `proxy.py` will start as many acceptor processes as there are CPU cores on the machine
+  - Use `--num-acceptors` flag to customize number of acceptor processes
+  - See [High Level Architecture](#high-level-architecture) to understand relationship between acceptors and workers
+
 - `Started server on ::1:8899`
   - By default, `proxy.py` listens on IPv6 `::1`, which is equivalent of IPv4 `127.0.0.1`
   - If you want to access `proxy.py` from external host, use `--hostname ::` or `--hostname 0.0.0.0` or bind to any other interface available on your machine.
@@ -1255,10 +1261,10 @@ import proxy
 
 if __name__ == '__main__':
   with proxy.Proxy([]) as p:
-    print(p.pool.flags.port)
+    print(p.acceptors.flags.port)
 ```
 
-`pool.flags.port` will give you access to the random port allocated by the kernel.
+`acceptors.flags.port` will give you access to the random port allocated by the kernel.
 
 ## Loading Plugins
 
@@ -1317,8 +1323,8 @@ Note that:
 
 1. `proxy.TestCase` overrides `unittest.TestCase.run()` method to setup and teardown `proxy.py`.
 2. `proxy.py` server will listen on a random available port on the system.
-   This random port is available as `self.PROXY.pool.flags.port` within your test cases.
-3. Only a single worker is started by default (`--num-workers 1`) for faster setup and teardown.
+   This random port is available as `self.PROXY.acceptors.flags.port` within your test cases.
+3. Only a single acceptor and worker is started by default (`--num-workers 1 --num-acceptors 1`) for faster setup and teardown.
 4. Most importantly, `proxy.TestCase` also ensures `proxy.py` server
    is up and running before proceeding with execution of tests. By default,
    `proxy.TestCase` will wait for `10 seconds` for `proxy.py` server to start,
@@ -1333,7 +1339,8 @@ Example:
 class TestProxyPyEmbedded(TestCase):
 
     PROXY_PY_STARTUP_FLAGS = [
-        '--num-workers', '1',
+        '--num-workers', '2',
+        '--num-acceptors', '1',
         '--enable-web-server',
     ]
 
@@ -1363,6 +1370,7 @@ class TestProxyPyEmbedded(unittest.TestCase):
     def run(self, result: Optional[unittest.TestResult] = None) -> Any:
         with proxy.start([
                 '--num-workers', '1',
+                '--num-acceptors', '1',
                 '--port', '... random port ...']):
             super().run(result)
 ```
@@ -1844,11 +1852,14 @@ ssl.SSLEOFError: EOF occurred in violation of protocol (_ssl.c:997)
 will try to utilize all available CPU cores to it for accepting new
 client connections. This is achieved by starting `AcceptorPool` which
 listens on configured server port. Then, `AcceptorPool` starts `Acceptor`
-processes (`--num-workers`) to accept incoming client connections.
+processes (`--num-acceptors`) to accept incoming client connections.
+Alongside, if `--threadless` is enabled, `ThreadlessPool` is setup
+which starts `Threadless` processes (`--num-workers`) to handle
+the incoming client connections.
 
 Each `Acceptor` process delegates the accepted client connection
-to a `Work` class.  Currently, `HttpProtocolHandler` is the default
-work klass hardcoded into the code.
+to a threadless process via `Work` class.  Currently, `HttpProtocolHandler`
+is the default work klass.
 
 `HttpProtocolHandler` simply assumes that incoming clients will follow
 HTTP specification.  Specific HTTP proxy and HTTP server implementations
@@ -1916,23 +1927,24 @@ for list of tests.
 
 ```console
 ❯ proxy -h
-usage: proxy [-h] [--enable-events] [--enable-conn-pool] [--threadless] [--threaded]
-          [--pid-file PID_FILE] [--backlog BACKLOG] [--hostname HOSTNAME]
-          [--port PORT] [--num-workers NUM_WORKERS]
+usage: -m [-h] [--enable-events] [--enable-conn-pool] [--threadless] [--threaded]
+          [--num-workers NUM_WORKERS] [--pid-file PID_FILE] [--backlog BACKLOG]
+          [--hostname HOSTNAME] [--port PORT] [--num-acceptors NUM_ACCEPTORS]
           [--unix-socket-path UNIX_SOCKET_PATH]
           [--client-recvbuf-size CLIENT_RECVBUF_SIZE] [--key-file KEY_FILE]
           [--timeout TIMEOUT] [--version] [--log-level LOG_LEVEL]
           [--log-file LOG_FILE] [--log-format LOG_FORMAT]
-          [--open-file-limit OPEN_FILE_LIMIT] [--plugins PLUGINS] [--enable-dashboard]
-          [--disable-http-proxy] [--ca-key-file CA_KEY_FILE]
-          [--ca-cert-dir CA_CERT_DIR] [--ca-cert-file CA_CERT_FILE]
-          [--ca-file CA_FILE] [--ca-signing-key-file CA_SIGNING_KEY_FILE]
-          [--cert-file CERT_FILE] [--disable-headers DISABLE_HEADERS]
+          [--open-file-limit OPEN_FILE_LIMIT] [--plugins PLUGINS]
+          [--enable-dashboard] [--work-klass WORK_KLASS] [--disable-http-proxy]
+          [--ca-key-file CA_KEY_FILE] [--ca-cert-dir CA_CERT_DIR]
+          [--ca-cert-file CA_CERT_FILE] [--ca-file CA_FILE]
+          [--ca-signing-key-file CA_SIGNING_KEY_FILE] [--cert-file CERT_FILE]
+          [--disable-headers DISABLE_HEADERS]
           [--server-recvbuf-size SERVER_RECVBUF_SIZE] [--basic-auth BASIC_AUTH]
-          [--cache-dir CACHE_DIR] [--filtered-upstream-hosts FILTERED_UPSTREAM_HOSTS]
-          [--enable-web-server] [--enable-static-server]
-          [--static-server-dir STATIC_SERVER_DIR] [--pac-file PAC_FILE]
-          [--pac-file-url-path PAC_FILE_URL_PATH]
+          [--cache-dir CACHE_DIR]
+          [--filtered-upstream-hosts FILTERED_UPSTREAM_HOSTS] [--enable-web-server]
+          [--enable-static-server] [--static-server-dir STATIC_SERVER_DIR]
+          [--pac-file PAC_FILE] [--pac-file-url-path PAC_FILE_URL_PATH]
           [--filtered-client-ips FILTERED_CLIENT_IPS]
           [--filtered-url-regex-config FILTERED_URL_REGEX_CONFIG]
           [--cloudflare-dns-mode CLOUDFLARE_DNS_MODE]
@@ -1944,75 +1956,82 @@ options:
   --enable-events       Default: False. Enables core to dispatch lifecycle events.
                         Plugins can be used to subscribe for core events.
   --enable-conn-pool    Default: False. (WIP) Enable upstream connection pooling.
-  --threadless          Default: True. Enabled by default on Python 3.8+ (mac, linux).
-                        When disabled a new thread is spawned to handle each client
-                        connection.
+  --threadless          Default: True. Enabled by default on Python 3.8+ (mac,
+                        linux). When disabled a new thread is spawned to handle
+                        each client connection.
   --threaded            Default: False. Disabled by default on Python < 3.8 and
-                        windows. When enabled a new thread is spawned to handle each
-                        client connection.
+                        windows. When enabled a new thread is spawned to handle
+                        each client connection.
+  --num-workers NUM_WORKERS
+                        Defaults to number of CPU cores.
   --pid-file PID_FILE   Default: None. Save parent process ID to a file.
-  --backlog BACKLOG     Default: 100. Maximum number of pending connections to proxy
-                        server
+  --backlog BACKLOG     Default: 100. Maximum number of pending connections to
+                        proxy server
   --hostname HOSTNAME   Default: ::1. Server IP address.
   --port PORT           Default: 8899. Server port.
-  --num-workers NUM_WORKERS
+  --num-acceptors NUM_ACCEPTORS
                         Defaults to number of CPU cores.
   --unix-socket-path UNIX_SOCKET_PATH
-                        Default: None. Unix socket path to use. When provided --host
-                        and --port flags are ignored
+                        Default: None. Unix socket path to use. When provided
+                        --host and --port flags are ignored
   --client-recvbuf-size CLIENT_RECVBUF_SIZE
-                        Default: 1 MB. Maximum amount of data received from the client
-                        in a single recv() operation. Bump this value for faster
-                        uploads at the expense of increased RAM.
+                        Default: 1 MB. Maximum amount of data received from the
+                        client in a single recv() operation. Bump this value for
+                        faster uploads at the expense of increased RAM.
   --key-file KEY_FILE   Default: None. Server key file to enable end-to-end TLS
-                        encryption with clients. If used, must also pass --cert-file.
+                        encryption with clients. If used, must also pass --cert-
+                        file.
   --timeout TIMEOUT     Default: 10.0. Number of seconds after which an inactive
-                        connection must be dropped. Inactivity is defined by no data
-                        sent or received by the client.
+                        connection must be dropped. Inactivity is defined by no
+                        data sent or received by the client.
   --version, -v         Prints proxy.py version.
   --log-level LOG_LEVEL
                         Valid options: DEBUG, INFO (default), WARNING, ERROR,
-                        CRITICAL. Both upper and lowercase values are allowed. You may
-                        also simply use the leading character e.g. --log-level d
+                        CRITICAL. Both upper and lowercase values are allowed. You
+                        may also simply use the leading character e.g. --log-level
+                        d
   --log-file LOG_FILE   Default: sys.stdout. Log file destination.
   --log-format LOG_FORMAT
                         Log format for Python logger.
   --open-file-limit OPEN_FILE_LIMIT
-                        Default: 1024. Maximum number of files (TCP connections) that
-                        proxy.py can open concurrently.
+                        Default: 1024. Maximum number of files (TCP connections)
+                        that proxy.py can open concurrently.
   --plugins PLUGINS     Comma separated plugins
   --enable-dashboard    Default: False. Enables proxy.py dashboard.
+  --work-klass WORK_KLASS
+                        Default: proxy.http.handler.HttpProtocolHandler. Work klass
+                        to use for work execution.
   --disable-http-proxy  Default: False. Whether to disable proxy.HttpProxyPlugin.
   --ca-key-file CA_KEY_FILE
-                        Default: None. CA key to use for signing dynamically generated
-                        HTTPS certificates. If used, must also pass --ca-cert-file and
-                        --ca-signing-key-file
+                        Default: None. CA key to use for signing dynamically
+                        generated HTTPS certificates. If used, must also pass --ca-
+                        cert-file and --ca-signing-key-file
   --ca-cert-dir CA_CERT_DIR
-                        Default: ~/.proxy.py. Directory to store dynamically generated
-                        certificates. Also see --ca-key-file, --ca-cert-file and --ca-
-                        signing-key-file
+                        Default: ~/.proxy.py. Directory to store dynamically
+                        generated certificates. Also see --ca-key-file, --ca-cert-
+                        file and --ca-signing-key-file
   --ca-cert-file CA_CERT_FILE
                         Default: None. Signing certificate to use for signing
-                        dynamically generated HTTPS certificates. If used, must also
-                        pass --ca-key-file and --ca-signing-key-file
-  --ca-file CA_FILE     Default:
-                        /Users/abhinavsingh/Dev/proxy.py/venv310/lib/python3.10/site-
-                        packages/certifi/cacert.pem. Provide path to custom CA bundle
-                        for peer certificate verification
+                        dynamically generated HTTPS certificates. If used, must
+                        also pass --ca-key-file and --ca-signing-key-file
+  --ca-file CA_FILE     Default: /Users/abhinavsingh/Dev/proxy.py/venv310/lib/pytho
+                        n3.10/site-packages/certifi/cacert.pem. Provide path to
+                        custom CA bundle for peer certificate verification
   --ca-signing-key-file CA_SIGNING_KEY_FILE
-                        Default: None. CA signing key to use for dynamic generation of
-                        HTTPS certificates. If used, must also pass --ca-key-file and
-                        --ca-cert-file
+                        Default: None. CA signing key to use for dynamic generation
+                        of HTTPS certificates. If used, must also pass --ca-key-
+                        file and --ca-cert-file
   --cert-file CERT_FILE
                         Default: None. Server certificate to enable end-to-end TLS
-                        encryption with clients. If used, must also pass --key-file.
+                        encryption with clients. If used, must also pass --key-
+                        file.
   --disable-headers DISABLE_HEADERS
                         Default: None. Comma separated list of headers to remove
                         before dispatching client request to upstream server.
   --server-recvbuf-size SERVER_RECVBUF_SIZE
-                        Default: 1 MB. Maximum amount of data received from the server
-                        in a single recv() operation. Bump this value for faster
-                        downloads at the expense of increased RAM.
+                        Default: 1 MB. Maximum amount of data received from the
+                        server in a single recv() operation. Bump this value for
+                        faster downloads at the expense of increased RAM.
   --basic-auth BASIC_AUTH
                         Default: No authentication. Specify colon separated
                         user:password to enable basic authentication.
@@ -2022,32 +2041,36 @@ options:
   --filtered-upstream-hosts FILTERED_UPSTREAM_HOSTS
                         Default: Blocks Facebook. Comma separated list of IPv4 and
                         IPv6 addresses.
-  --enable-web-server   Default: False. Whether to enable proxy.HttpWebServerPlugin.
+  --enable-web-server   Default: False. Whether to enable
+                        proxy.HttpWebServerPlugin.
   --enable-static-server
-                        Default: False. Enable inbuilt static file server. Optionally,
-                        also use --static-server-dir to serve static content from
-                        custom directory. By default, static file server serves out of
-                        installed proxy.py python module folder.
+                        Default: False. Enable inbuilt static file server.
+                        Optionally, also use --static-server-dir to serve static
+                        content from custom directory. By default, static file
+                        server serves out of installed proxy.py python module
+                        folder.
   --static-server-dir STATIC_SERVER_DIR
                         Default: "public" folder in directory where proxy.py is
-                        placed. This option is only applicable when static server is
-                        also enabled. See --enable-static-server.
-  --pac-file PAC_FILE   A file (Proxy Auto Configuration) or string to serve when the
-                        server receives a direct file request. Using this option
-                        enables proxy.HttpWebServerPlugin.
+                        placed. This option is only applicable when static server
+                        is also enabled. See --enable-static-server.
+  --pac-file PAC_FILE   A file (Proxy Auto Configuration) or string to serve when
+                        the server receives a direct file request. Using this
+                        option enables proxy.HttpWebServerPlugin.
   --pac-file-url-path PAC_FILE_URL_PATH
                         Default: /. Web server path to serve the PAC file.
   --filtered-client-ips FILTERED_CLIENT_IPS
-                        Default: 127.0.0.1,::1. Comma separated list of IPv4 and IPv6
-                        addresses.
+                        Default: 127.0.0.1,::1. Comma separated list of IPv4 and
+                        IPv6 addresses.
   --filtered-url-regex-config FILTERED_URL_REGEX_CONFIG
                         Default: No config. Comma separated list of IPv4 and IPv6
                         addresses.
   --cloudflare-dns-mode CLOUDFLARE_DNS_MODE
-                        Default: security. Either "security" (for malware protection)
-                        or "family" (for malware and adult content protection)
+                        Default: security. Either "security" (for malware
+                        protection) or "family" (for malware and adult content
+                        protection)
 
-Proxy.py not working? Report at: https://github.com/abhinavsingh/proxy.py/issues/new
+Proxy.py not working? Report at:
+https://github.com/abhinavsingh/proxy.py/issues/new
 ```
 
 # Changelog
diff --git a/examples/https_connect_tunnel.py b/examples/https_connect_tunnel.py
index f98b3e4..5b246a5 100644
--- a/examples/https_connect_tunnel.py
+++ b/examples/https_connect_tunnel.py
@@ -12,12 +12,11 @@ import time
 
 from typing import Any, Optional
 
-from proxy.common.flag import FlagParser
+from proxy import Proxy
 from proxy.common.utils import build_http_response
 from proxy.http.codes import httpStatusCodes
 from proxy.http.parser import httpParserStates
 from proxy.http.methods import httpMethods
-from proxy.core.acceptor import AcceptorPool
 from proxy.core.base import BaseTcpTunnelHandler
 
 
@@ -75,11 +74,11 @@ class HttpsConnectTunnelHandler(BaseTcpTunnelHandler):
 
 def main() -> None:
     # This example requires `threadless=True`
-    with AcceptorPool(
-            flags=FlagParser.initialize(
-                port=12345, num_workers=1, threadless=True,
-            ),
-            work_klass=HttpsConnectTunnelHandler,
+    with Proxy(
+        work_klass=HttpsConnectTunnelHandler,
+        threadless=True,
+        num_workers=1,
+        port=12345,
     ):
         try:
             while True:
diff --git a/examples/ssl_echo_server.py b/examples/ssl_echo_server.py
index b02f1f2..65432f3 100644
--- a/examples/ssl_echo_server.py
+++ b/examples/ssl_echo_server.py
@@ -11,9 +11,8 @@
 import time
 from typing import Optional
 
-from proxy.common.flag import FlagParser
+from proxy import Proxy
 from proxy.common.utils import wrap_socket
-from proxy.core.acceptor import AcceptorPool
 from proxy.core.connection import TcpClientConnection
 
 from proxy.core.base import BaseTcpServerHandler
@@ -45,15 +44,13 @@ class EchoSSLServerHandler(BaseTcpServerHandler):
 
 def main() -> None:
     # This example requires `threadless=True`
-    with AcceptorPool(
-        flags=FlagParser.initialize(
-            port=12345,
-            num_workers=1,
-            threadless=True,
-            keyfile='https-key.pem',
-            certfile='https-signed-cert.pem',
-        ),
+    with Proxy(
         work_klass=EchoSSLServerHandler,
+        threadless=True,
+        num_workers=1,
+        port=12345,
+        keyfile='https-key.pem',
+        certfile='https-signed-cert.pem',
     ):
         try:
             while True:
diff --git a/examples/tcp_echo_server.py b/examples/tcp_echo_server.py
index 8453c95..cd49241 100644
--- a/examples/tcp_echo_server.py
+++ b/examples/tcp_echo_server.py
@@ -11,8 +11,7 @@
 import time
 from typing import Optional
 
-from proxy.common.flag import FlagParser
-from proxy.core.acceptor import AcceptorPool
+from proxy import Proxy
 from proxy.core.base import BaseTcpServerHandler
 
 
@@ -30,11 +29,11 @@ class EchoServerHandler(BaseTcpServerHandler):
 
 def main() -> None:
     # This example requires `threadless=True`
-    with AcceptorPool(
-        flags=FlagParser.initialize(
-            port=12345, num_workers=1, threadless=True,
-        ),
+    with Proxy(
         work_klass=EchoServerHandler,
+        threadless=True,
+        num_workers=1,
+        port=12345,
     ):
         try:
             while True:
diff --git a/examples/web_scraper.py b/examples/web_scraper.py
index 27f7953..0cd638a 100644
--- a/examples/web_scraper.py
+++ b/examples/web_scraper.py
@@ -13,8 +13,8 @@ import socket
 
 from typing import Dict
 
-from proxy.common.flag import FlagParser
-from proxy.core.acceptor import Work, AcceptorPool
+from proxy import Proxy
+from proxy.core.acceptor import Work
 from proxy.common.types import Readables, Writables
 
 
@@ -56,15 +56,11 @@ class WebScraper(Work):
 
 
 if __name__ == '__main__':
-    with AcceptorPool(
-        flags=FlagParser.initialize(
-            port=12345,
-            num_workers=1,
-            threadless=True,
-            keyfile='https-key.pem',
-            certfile='https-signed-cert.pem',
-        ),
+    with Proxy(
         work_klass=WebScraper,
+        threadless=True,
+        num_workers=1,
+        port=12345,
     ) as pool:
         while True:
             time.sleep(1)
diff --git a/proxy/common/constants.py b/proxy/common/constants.py
index 757f09e..b5b8818 100644
--- a/proxy/common/constants.py
+++ b/proxy/common/constants.py
@@ -87,6 +87,7 @@ DEFAULT_HTTP_ACCESS_LOG_FORMAT = '{client_ip}:{client_port} - ' + \
 DEFAULT_HTTPS_ACCESS_LOG_FORMAT = '{client_ip}:{client_port} - ' + \
     '{request_method} {server_host}:{server_port} - ' + \
     '{response_bytes} bytes - {connection_time_ms} ms'
+DEFAULT_NUM_ACCEPTORS = 0
 DEFAULT_NUM_WORKERS = 0
 DEFAULT_OPEN_FILE_LIMIT = 1024
 DEFAULT_PAC_FILE = None
@@ -102,6 +103,7 @@ DEFAULT_VERSION = False
 DEFAULT_HTTP_PORT = 80
 DEFAULT_HTTPS_PORT = 443
 DEFAULT_MAX_SEND_SIZE = 16 * 1024
+DEFAULT_WORK_KLASS = 'proxy.http.HttpProtocolHandler'
 
 DEFAULT_DEVTOOLS_DOC_URL = 'http://proxy'
 DEFAULT_DEVTOOLS_FRAME_ID = secrets.token_hex(8)
diff --git a/proxy/common/flag.py b/proxy/common/flag.py
index a340617..88609d5 100644
--- a/proxy/common/flag.py
+++ b/proxy/common/flag.py
@@ -22,7 +22,7 @@ from typing import Optional, List, Any, cast
 from .plugins import Plugins
 from .types import IpAddress
 from .utils import text_, bytes_, is_py2, set_open_file_limit
-from .constants import COMMA, DEFAULT_DATA_DIRECTORY_PATH, DEFAULT_NUM_WORKERS
+from .constants import COMMA, DEFAULT_DATA_DIRECTORY_PATH, DEFAULT_NUM_ACCEPTORS, DEFAULT_NUM_WORKERS
 from .constants import DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HEADERS, PY2_DEPRECATION_MESSAGE
 from .constants import PLUGIN_DASHBOARD, PLUGIN_DEVTOOLS_PROTOCOL
 from .constants import PLUGIN_HTTP_PROXY, PLUGIN_INSPECT_TRAFFIC, PLUGIN_PAC_FILE
@@ -84,8 +84,8 @@ class FlagParser:
 
     @staticmethod
     def initialize(
-        input_args: Optional[List[str]]
-        = None, **opts: Any,
+        input_args: Optional[List[str]] = None,
+        **opts: Any,
     ) -> argparse.Namespace:
         if input_args is None:
             input_args = []
@@ -107,13 +107,35 @@ class FlagParser:
             print(__version__)
             sys.exit(0)
 
+        # proxy.py currently cannot serve over HTTPS and also perform TLS interception
+        # at the same time.  Check if user is trying to enable both feature
+        # at the same time.
+        if (args.cert_file and args.key_file) and \
+                (args.ca_key_file and args.ca_cert_file and args.ca_signing_key_file):
+            print(
+                'You can either enable end-to-end encryption OR TLS interception,'
+                'not both together.',
+            )
+            sys.exit(1)
+
         # Setup logging module
         Logger.setup_logger(args.log_file, args.log_level, args.log_format)
 
         # Setup limits
         set_open_file_limit(args.open_file_limit)
 
-        # Load plugins
+        # Load work_klass
+        work_klass = opts.get('work_klass', args.work_klass)
+        work_klass = Plugins.importer(bytes_(work_klass))[0] \
+            if isinstance(work_klass, str) \
+            else work_klass
+
+        # Generate auth_code required for basic authentication if enabled
+        auth_code = None
+        if args.basic_auth:
+            auth_code = base64.b64encode(bytes_(args.basic_auth))
+
+        # Load default plugins along with user provided --plugins
         default_plugins = [
             bytes_(p)
             for p in FlagParser.get_default_plugins(args)
@@ -123,31 +145,14 @@ class FlagParser:
             for p in opts.get('plugins', args.plugins.split(text_(COMMA)))
             if not (isinstance(p, str) and len(p) == 0)
         ]
-
-        # Load default plugins along with user provided --plugins
         plugins = Plugins.load(default_plugins + extra_plugins)
 
-        # proxy.py currently cannot serve over HTTPS and also perform TLS interception
-        # at the same time.  Check if user is trying to enable both feature
-        # at the same time.
-        if (args.cert_file and args.key_file) and \
-                (args.ca_key_file and args.ca_cert_file and args.ca_signing_key_file):
-            print(
-                'You can either enable end-to-end encryption OR TLS interception,'
-                'not both together.',
-            )
-            sys.exit(1)
-
-        # Generate auth_code required for basic authentication if enabled
-        auth_code = None
-        if args.basic_auth:
-            auth_code = base64.b64encode(bytes_(args.basic_auth))
-
         # https://github.com/python/mypy/issues/5865
         #
         # def option(t: object, key: str, default: Any) -> Any:
         #     return cast(t, opts.get(key, default))
 
+        args.work_klass = work_klass
         args.plugins = plugins
         args.auth_code = cast(
             Optional[bytes],
@@ -235,18 +240,33 @@ class FlagParser:
                 socket.AF_INET6 if args.hostname.version == 6 else socket.AF_INET
             )
         else:
-            # FIXME: Not true for tests, as this value will be mock
+            # FIXME: Not true for tests, as this value will be a mock.
+            #
             # It's a problem only on Windows.  Instead of a proper
-            # test level fix, simply commenting this for now.
+            # fix in the tests, simply commenting this line of assertion
+            # for now.
+            #
             # assert args.unix_socket_path is None
             args.family = socket.AF_INET6 if args.hostname.version == 6 else socket.AF_INET
         args.port = cast(int, opts.get('port', args.port))
         args.backlog = cast(int, opts.get('backlog', args.backlog))
         num_workers = opts.get('num_workers', args.num_workers)
-        num_workers = num_workers if num_workers is not None else DEFAULT_NUM_WORKERS
         args.num_workers = cast(
             int, num_workers if num_workers > 0 else multiprocessing.cpu_count(),
         )
+        num_acceptors = opts.get('num_acceptors', args.num_acceptors)
+        # See https://github.com/abhinavsingh/proxy.py/pull/714 description
+        # to understand rationale behind the following logic.
+        #
+        # --num-workers flag or option was found. We will use
+        # the same value for num_acceptors when --num-acceptors flag
+        # is absent.
+        if num_workers != DEFAULT_NUM_WORKERS and num_acceptors == DEFAULT_NUM_ACCEPTORS:
+            args.num_acceptors = args.num_workers
+        else:
+            args.num_acceptors = cast(
+                int, num_acceptors if num_acceptors > 0 else multiprocessing.cpu_count(),
+            )
         args.static_server_dir = cast(
             str,
             opts.get(
diff --git a/proxy/core/acceptor/__init__.py b/proxy/core/acceptor/__init__.py
index cca3bcd..7bc92a8 100644
--- a/proxy/core/acceptor/__init__.py
+++ b/proxy/core/acceptor/__init__.py
@@ -12,10 +12,12 @@ from .acceptor import Acceptor
 from .pool import AcceptorPool
 from .work import Work
 from .threadless import Threadless
+from .executors import ThreadlessPool
 
 __all__ = [
     'Acceptor',
     'AcceptorPool',
     'Work',
     'Threadless',
+    'ThreadlessPool',
 ]
diff --git a/proxy/core/acceptor/acceptor.py b/proxy/core/acceptor/acceptor.py
index d5eb695..18758f5 100644
--- a/proxy/core/acceptor/acceptor.py
+++ b/proxy/core/acceptor/acceptor.py
@@ -8,52 +8,28 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
-import argparse
+import socket
 import logging
+import argparse
+import selectors
 import multiprocessing
 import multiprocessing.synchronize
-import selectors
-import socket
-import threading
 
 from multiprocessing import connection
-from multiprocessing.reduction import send_handle, recv_handle
-from typing import Optional, Type, Tuple
+from multiprocessing.reduction import recv_handle
+
+from typing import List, Optional, Tuple
+
+from proxy.core.acceptor.executors import ThreadlessPool
 
-from .work import Work
-from .threadless import Threadless
+from ..event import EventQueue
 
-from ..connection import TcpClientConnection
-from ..event import EventQueue, eventNames
-from ...common.constants import DEFAULT_THREADLESS
-from ...common.flag import flags
 from ...common.utils import is_threadless
 from ...common.logger import Logger
 
 logger = logging.getLogger(__name__)
 
 
-flags.add_argument(
-    '--threadless',
-    action='store_true',
-    default=DEFAULT_THREADLESS,
-    help='Default: ' + ('True' if DEFAULT_THREADLESS else 'False') + '.  ' +
-    'Enabled by default on Python 3.8+ (mac, linux).  ' +
-    'When disabled a new thread is spawned '
-    'to handle each client connection.',
-)
-
-flags.add_argument(
-    '--threaded',
-    action='store_true',
-    default=not DEFAULT_THREADLESS,
-    help='Default: ' + ('True' if not DEFAULT_THREADLESS else 'False') + '.  ' +
-    'Disabled by default on Python < 3.8 and windows.  ' +
-    'When enabled a new thread is spawned '
-    'to handle each client connection.',
-)
-
-
 class Acceptor(multiprocessing.Process):
     """Work acceptor process.
 
@@ -81,8 +57,9 @@ class Acceptor(multiprocessing.Process):
             idd: int,
             work_queue: connection.Connection,
             flags: argparse.Namespace,
-            work_klass: Type[Work],
             lock: multiprocessing.synchronize.Lock,
+            executor_queues: List[connection.Connection],
+            executor_pids: List[int],
             event_queue: Optional[EventQueue] = None,
     ) -> None:
         super().__init__()
@@ -96,63 +73,17 @@ class Acceptor(multiprocessing.Process):
         self.lock = lock
         # Queue over which server socket fd is received on start-up
         self.work_queue: connection.Connection = work_queue
-        # Worker class
-        self.work_klass = work_klass
-        # Selector & threadless states
+        # Available executors
+        self.executor_queues = executor_queues
+        self.executor_pids = executor_pids
+        # Selector
         self.running = multiprocessing.Event()
         self.selector: Optional[selectors.DefaultSelector] = None
-        self.threadless_process: Optional[Threadless] = None
-        self.threadless_client_queue: Optional[connection.Connection] = None
         # File descriptor used to accept new work
         # Currently, a socket fd is assumed.
         self.sock: Optional[socket.socket] = None
-
-    def start_threadless_process(self) -> None:
-        pipe = multiprocessing.Pipe()
-        self.threadless_client_queue = pipe[0]
-        self.threadless_process = Threadless(
-            client_queue=pipe[1],
-            flags=self.flags,
-            work_klass=self.work_klass,
-            event_queue=self.event_queue,
-        )
-        self.threadless_process.start()
-        logger.debug('Started process %d', self.threadless_process.pid)
-
-    def shutdown_threadless_process(self) -> None:
-        assert self.threadless_process and self.threadless_client_queue
-        logger.debug('Stopped process %d', self.threadless_process.pid)
-        self.threadless_process.running.set()
-        self.threadless_process.join()
-        self.threadless_client_queue.close()
-
-    def _start_threadless_work(self, conn: socket.socket, addr: Optional[Tuple[str, int]]) -> None:
-        assert self.threadless_process and self.threadless_client_queue
-        # Accepted client address is empty string for
-        # unix socket domain, avoid sending empty string
-        if not self.flags.unix_socket_path:
-            self.threadless_client_queue.send(addr)
-        send_handle(
-            self.threadless_client_queue,
-            conn.fileno(),
-            self.threadless_process.pid,
-        )
-        conn.close()
-
-    def _start_threaded_work(self, conn: socket.socket, addr: Optional[Tuple[str, int]]) -> None:
-        work = self.work_klass(
-            TcpClientConnection(conn, addr),
-            flags=self.flags,
-            event_queue=self.event_queue,
-        )
-        work_thread = threading.Thread(target=work.run)
-        work_thread.daemon = True
-        work.publish_event(
-            event_name=eventNames.WORK_STARTED,
-            event_payload={'fileno': conn.fileno(), 'addr': addr},
-            publisher_id=self.__class__.__name__,
-        )
-        work_thread.start()
+        # Incremented every time work() is called
+        self._total: int = 0
 
     def run_once(self) -> None:
         with self.lock:
@@ -162,14 +93,7 @@ class Acceptor(multiprocessing.Process):
                 return
             conn, addr = self.sock.accept()
         addr = None if addr == '' else addr
-        if (
-                is_threadless(self.flags.threadless, self.flags.threaded) and
-                self.threadless_client_queue and
-                self.threadless_process
-        ):
-            self._start_threadless_work(conn, addr)
-        else:
-            self._start_threaded_work(conn, addr)
+        self._work(conn, addr)
 
     def run(self) -> None:
         Logger.setup_logger(
@@ -186,15 +110,45 @@ class Acceptor(multiprocessing.Process):
         )
         try:
             self.selector.register(self.sock, selectors.EVENT_READ)
-            if is_threadless(self.flags.threadless, self.flags.threaded):
-                self.start_threadless_process()
             while not self.running.is_set():
                 self.run_once()
         except KeyboardInterrupt:
             pass
         finally:
             self.selector.unregister(self.sock)
-            if is_threadless(self.flags.threadless, self.flags.threaded):
-                self.shutdown_threadless_process()
             self.sock.close()
             logger.debug('Acceptor#%d shutdown', self.idd)
+
+    def _work(self, conn: socket.socket, addr: Optional[Tuple[str, int]]) -> None:
+        if is_threadless(self.flags.threadless, self.flags.threaded):
+            # Index of worker to which this work should be dispatched
+            # Use round-robin strategy by default.
+            #
+            # By default all acceptors will start sending work to
+            # 1st workers.  To randomize, we offset index by idd.
+            index = (self._total + self.idd) % self.flags.num_workers
+            ThreadlessPool.delegate(
+                self.executor_pids[index],
+                self.executor_queues[index],
+                conn,
+                addr,
+                self.flags.unix_socket_path,
+            )
+            logger.debug(
+                'Dispatched work#{0}.{1} to worker#{2}'.format(
+                    self.idd, self._total, index,
+                ),
+            )
+        else:
+            _, thread = ThreadlessPool.start_threaded_work(
+                self.flags,
+                conn, addr,
+                event_queue=self.event_queue,
+                publisher_id=self.__class__.__name__,
+            )
+            logger.debug(
+                'Started work#{0}.{1} in thread#{2}'.format(
+                    self.idd, self._total, thread.ident,
+                ),
+            )
+        self._total += 1
diff --git a/proxy/core/acceptor/executors.py b/proxy/core/acceptor/executors.py
new file mode 100644
index 0000000..0070fad
--- /dev/null
+++ b/proxy/core/acceptor/executors.py
@@ -0,0 +1,198 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
+import socket
+import logging
+import argparse
+import threading
+import multiprocessing
+
+from multiprocessing import connection
+from multiprocessing.reduction import send_handle
+
+from typing import Any, Optional, List, Tuple
+
+from .work import Work
+from .threadless import Threadless
+
+from ..connection import TcpClientConnection
+from ..event import EventQueue, eventNames
+
+from ...common.flag import flags
+from ...common.utils import is_threadless
+from ...common.constants import DEFAULT_NUM_WORKERS, DEFAULT_THREADLESS
+
+logger = logging.getLogger(__name__)
+
+
+flags.add_argument(
+    '--threadless',
+    action='store_true',
+    default=DEFAULT_THREADLESS,
+    help='Default: ' + ('True' if DEFAULT_THREADLESS else 'False') + '.  ' +
+    'Enabled by default on Python 3.8+ (mac, linux).  ' +
+    'When disabled a new thread is spawned '
+    'to handle each client connection.',
+)
+
+flags.add_argument(
+    '--threaded',
+    action='store_true',
+    default=not DEFAULT_THREADLESS,
+    help='Default: ' + ('True' if not DEFAULT_THREADLESS else 'False') + '.  ' +
+    'Disabled by default on Python < 3.8 and windows.  ' +
+    'When enabled a new thread is spawned '
+    'to handle each client connection.',
+)
+
+flags.add_argument(
+    '--num-workers',
+    type=int,
+    default=DEFAULT_NUM_WORKERS,
+    help='Defaults to number of CPU cores.',
+)
+
+
+class ThreadlessPool:
+    """Manages lifecycle of threadless pool and delegates work to them
+    using a round-robin strategy.
+
+    Example usage:
+
+        with ThreadlessPool(flags=...) as pool:
+            while True:
+                time.sleep(1)
+
+    If necessary, start multiple threadless pool with different
+    work classes.
+
+    TODO: We could optimize multiple-work-type scenario
+    by making Threadless class constructor independent of work_klass.
+    We could then relay the work_klass during work delegation.
+    This will also make ThreadlessPool constructor agnostic
+    of work_klass.
+    """
+
+    def __init__(
+        self,
+        flags: argparse.Namespace,
+        event_queue: Optional[EventQueue] = None,
+    ) -> None:
+        self.flags = flags
+        self.event_queue = event_queue
+        # Threadless worker communication states
+        self.work_queues: List[connection.Connection] = []
+        self.work_pids: List[int] = []
+        # List of threadless workers
+        self._workers: List[Threadless] = []
+
+    def __enter__(self) -> 'ThreadlessPool':
+        self.setup()
+        return self
+
+    def __exit__(self, *args: Any) -> None:
+        self.shutdown()
+
+    @staticmethod
+    def delegate(
+            worker_pid: int,
+            work_queue: connection.Connection,
+            conn: socket.socket,
+            addr: Optional[Tuple[str, int]],
+            unix_socket_path: Optional[str] = None,
+    ) -> None:
+        """Utility method to delegate a work to threadless executor pool."""
+        # Accepted client address is empty string for
+        # unix socket domain, avoid sending empty string
+        # for optimization.
+        if not unix_socket_path:
+            work_queue.send(addr)
+        send_handle(
+            work_queue,
+            conn.fileno(),
+            worker_pid,
+        )
+        conn.close()
+
+    @staticmethod
+    def start_threaded_work(
+            flags: argparse.Namespace,
+            conn: socket.socket,
+            addr: Optional[Tuple[str, int]],
+            event_queue: Optional[EventQueue] = None,
+            publisher_id: Optional[str] = None,
+    ) -> Tuple[Work, threading.Thread]:
+        """Utility method to start a work in a new thread."""
+        work = flags.work_klass(
+            TcpClientConnection(conn, addr),
+            flags=flags,
+            event_queue=event_queue,
+        )
+        # TODO: Keep reference to threads and join during shutdown.
+        # This will ensure connections are not abruptly closed on shutdown
+        # for threaded execution mode.
+        thread = threading.Thread(target=work.run)
+        thread.daemon = True
+        thread.start()
+        work.publish_event(
+            event_name=eventNames.WORK_STARTED,
+            event_payload={'fileno': conn.fileno(), 'addr': addr},
+            publisher_id=publisher_id or 'thread#{0}'.format(
+                thread.ident,
+            ),
+        )
+        return (work, thread)
+
+    def setup(self) -> None:
+        """Setup threadless processes."""
+        if is_threadless(self.flags.threadless, self.flags.threaded):
+            for index in range(self.flags.num_workers):
+                self._start_worker(index)
+            logger.info(
+                'Started {0} threadless workers'.format(
+                    self.flags.num_workers,
+                ),
+            )
+
+    def shutdown(self) -> None:
+        """Shutdown threadless processes."""
+        if is_threadless(self.flags.threadless, self.flags.threaded):
+            for _ in range(self.flags.num_workers):
+                self._shutdown_worker()
+            logger.info(
+                'Stopped {0} threadless workers'.format(
+                    self.flags.num_workers,
+                ),
+            )
+
+    def _start_worker(self, index: int) -> None:
+        """Starts a threadless worker."""
+        pipe = multiprocessing.Pipe()
+        self.work_queues.append(pipe[0])
+        w = Threadless(
+            client_queue=pipe[1],
+            flags=self.flags,
+            event_queue=self.event_queue,
+        )
+        self._workers.append(w)
+        w.start()
+        assert w.pid
+        self.work_pids.append(w.pid)
+        logger.debug('Started threadless#%d process#%d', index, w.pid)
+
+    def _shutdown_worker(self) -> None:
+        """Pop a running threadless worker and clean it up."""
+        w = self._workers.pop()
+        pid = w.pid
+        w.running.set()
+        w.join()
+        self.work_pids.pop()
+        self.work_queues.pop().close()
+        logger.debug('Stopped threadless process#%d', pid)
diff --git a/proxy/core/acceptor/pool.py b/proxy/core/acceptor/pool.py
index 426f51b..e5fa4d9 100644
--- a/proxy/core/acceptor/pool.py
+++ b/proxy/core/acceptor/pool.py
@@ -9,26 +9,24 @@
     :license: BSD, see LICENSE for more details.
 """
 import os
-import argparse
+import socket
 import logging
+import argparse
 import multiprocessing
-import socket
 
 from multiprocessing import connection
 from multiprocessing.reduction import send_handle
 
-from typing import List, Optional, Type
-from types import TracebackType
+from typing import Any, List, Optional
 
 from .acceptor import Acceptor
-from .work import Work
 
 from ..event import EventQueue
 
-from ...common.utils import bytes_, is_threadless
+from ...common.utils import bytes_
 from ...common.flag import flags
 from ...common.constants import DEFAULT_BACKLOG, DEFAULT_IPV6_HOSTNAME
-from ...common.constants import DEFAULT_NUM_WORKERS, DEFAULT_PORT
+from ...common.constants import DEFAULT_NUM_ACCEPTORS, DEFAULT_PORT
 from ...common.constants import DEFAULT_PID_FILE
 
 logger = logging.getLogger(__name__)
@@ -65,9 +63,9 @@ flags.add_argument(
 )
 
 flags.add_argument(
-    '--num-workers',
+    '--num-acceptors',
     type=int,
-    default=DEFAULT_NUM_WORKERS,
+    default=DEFAULT_NUM_ACCEPTORS,
     help='Defaults to number of CPU cores.',
 )
 
@@ -90,16 +88,18 @@ class AcceptorPool:
 
     Example usage:
 
-        with AcceptorPool(flags=..., work_klass=...) as pool:
+        with AcceptorPool(flags=...) as pool:
             while True:
                 time.sleep(1)
 
-    `work_klass` must implement `work.Work` class.
+    `flags.work_klass` must implement `work.Work` class.
     """
 
     def __init__(
         self, flags: argparse.Namespace,
-        work_klass: Type[Work], event_queue: Optional[EventQueue] = None,
+        executor_queues: List[connection.Connection],
+        executor_pids: List[int],
+        event_queue: Optional[EventQueue] = None,
     ) -> None:
         self.flags = flags
         # Eventing core queue
@@ -110,19 +110,15 @@ class AcceptorPool:
         self.acceptors: List[Acceptor] = []
         # Work queue used to share file descriptor with acceptor processes
         self.work_queues: List[connection.Connection] = []
-        # Work class implementation
-        self.work_klass = work_klass
+        # Available executors
+        self.executor_queues: List[connection.Connection] = executor_queues
+        self.executor_pids: List[int] = executor_pids
 
     def __enter__(self) -> 'AcceptorPool':
         self.setup()
         return self
 
-    def __exit__(
-            self,
-            exc_type: Optional[Type[BaseException]],
-            exc_val: Optional[BaseException],
-            exc_tb: Optional[TracebackType],
-    ) -> None:
+    def __exit__(self, *args: Any) -> None:
         self.shutdown()
 
     def setup(self) -> None:
@@ -137,10 +133,10 @@ class AcceptorPool:
         # the server port when `--port=0` is used.
         assert self.socket
         self.flags.port = self.socket.getsockname()[1]
-        self._start_acceptors()
+        self._start()
         # Send file descriptor to all acceptor processes.
         assert self.socket is not None
-        for index in range(self.flags.num_workers):
+        for index in range(self.flags.num_acceptors):
             send_handle(
                 self.work_queues[index],
                 self.socket.fileno(),
@@ -150,7 +146,7 @@ class AcceptorPool:
         self.socket.close()
 
     def shutdown(self) -> None:
-        logger.info('Shutting down %d workers' % self.flags.num_workers)
+        logger.info('Shutting down %d acceptors' % self.flags.num_acceptors)
         for acceptor in self.acceptors:
             acceptor.running.set()
         for acceptor in self.acceptors:
@@ -174,17 +170,18 @@ class AcceptorPool:
         self.socket.listen(self.flags.backlog)
         self.socket.setblocking(False)
 
-    def _start_acceptors(self) -> None:
+    def _start(self) -> None:
         """Start acceptor processes."""
-        for acceptor_id in range(self.flags.num_workers):
+        for acceptor_id in range(self.flags.num_acceptors):
             work_queue = multiprocessing.Pipe()
             acceptor = Acceptor(
                 idd=acceptor_id,
                 work_queue=work_queue[1],
                 flags=self.flags,
-                work_klass=self.work_klass,
                 lock=LOCK,
                 event_queue=self.event_queue,
+                executor_queues=self.executor_queues,
+                executor_pids=self.executor_pids,
             )
             acceptor.start()
             logger.debug(
@@ -194,10 +191,7 @@ class AcceptorPool:
             )
             self.acceptors.append(acceptor)
             self.work_queues.append(work_queue[0])
-        mode = 'threadless' if is_threadless(
-            self.flags.threadless, self.flags.threaded,
-        ) else 'threaded'
-        logger.info('Started %d %s workers' % (self.flags.num_workers, mode))
+        logger.info('Started %d acceptors' % self.flags.num_acceptors)
 
     def _write_pid_file(self) -> None:
         if self.flags.pid_file is not None:
diff --git a/proxy/core/acceptor/threadless.py b/proxy/core/acceptor/threadless.py
index 7a957f4..aa8c9c2 100644
--- a/proxy/core/acceptor/threadless.py
+++ b/proxy/core/acceptor/threadless.py
@@ -19,7 +19,7 @@ import multiprocessing
 
 from multiprocessing import connection
 from multiprocessing.reduction import recv_handle
-from typing import Dict, Optional, Tuple, List, Generator, Any, Type
+from typing import Dict, Optional, Tuple, List, Generator, Any
 
 from .work import Work
 
@@ -56,13 +56,11 @@ class Threadless(multiprocessing.Process):
             self,
             client_queue: connection.Connection,
             flags: argparse.Namespace,
-            work_klass: Type[Work],
             event_queue: Optional[EventQueue] = None,
     ) -> None:
         super().__init__()
         self.client_queue = client_queue
         self.flags = flags
-        self.work_klass = work_klass
         self.event_queue = event_queue
 
         self.running = multiprocessing.Event()
@@ -139,7 +137,7 @@ class Threadless(multiprocessing.Process):
         if not self.flags.unix_socket_path:
             addr = self.client_queue.recv()
         fileno = recv_handle(self.client_queue)
-        self.works[fileno] = self.work_klass(
+        self.works[fileno] = self.flags.work_klass(
             TcpClientConnection(conn=self.fromfd(fileno), addr=addr),
             flags=self.flags,
             event_queue=self.event_queue,
diff --git a/proxy/core/event/manager.py b/proxy/core/event/manager.py
index 7359c0a..20d5c6e 100644
--- a/proxy/core/event/manager.py
+++ b/proxy/core/event/manager.py
@@ -12,8 +12,7 @@ import logging
 import threading
 import multiprocessing
 
-from typing import Optional, Type
-from types import TracebackType
+from typing import Any, Optional
 
 from .queue import EventQueue
 from .dispatcher import EventDispatcher
@@ -50,12 +49,7 @@ class EventManager:
         self.setup()
         return self
 
-    def __exit__(
-            self,
-            exc_type: Optional[Type[BaseException]],
-            exc_val: Optional[BaseException],
-            exc_tb: Optional[TracebackType],
-    ) -> None:
+    def __exit__(self, *args: Any) -> None:
         self.shutdown()
 
     def setup(self) -> None:
diff --git a/proxy/http/__init__.py b/proxy/http/__init__.py
index 232621f..e293b97 100644
--- a/proxy/http/__init__.py
+++ b/proxy/http/__init__.py
@@ -8,3 +8,10 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
+from .handler import HttpProtocolHandler
+from .plugin import HttpProtocolHandlerPlugin
+
+__all__ = [
+    'HttpProtocolHandler',
+    'HttpProtocolHandlerPlugin',
+]
diff --git a/proxy/proxy.py b/proxy/proxy.py
index 583e8d2..e20b7ec 100644
--- a/proxy/proxy.py
+++ b/proxy/proxy.py
@@ -12,18 +12,14 @@ import sys
 import time
 import logging
 
-from types import TracebackType
-from typing import List, Optional, Any, Type
+from typing import List, Optional, Any
 
-from proxy.core.acceptor.work import Work
-
-from .core.acceptor import AcceptorPool
-from .http.handler import HttpProtocolHandler
+from .core.acceptor import AcceptorPool, ThreadlessPool
 from .core.event import EventManager
 from .common.flag import FlagParser, flags
 from .common.constants import DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_LOG_LEVEL
 from .common.constants import DEFAULT_OPEN_FILE_LIMIT, DEFAULT_PLUGINS, DEFAULT_VERSION
-from .common.constants import DEFAULT_ENABLE_DASHBOARD
+from .common.constants import DEFAULT_ENABLE_DASHBOARD, DEFAULT_WORK_KLASS
 
 
 logger = logging.getLogger(__name__)
@@ -77,10 +73,10 @@ flags.add_argument(
 
 # TODO: Ideally all `--enable-*` flags must be at the top-level.
 # --enable-dashboard is specially needed here because
-# ProxyDashboard class is not imported by anyone.
+# ProxyDashboard class is not imported anywhere.
 #
-# If we move this flag definition within dashboard.py,
-# users will also have to explicitly enable dashboard plugin
+# Due to which, if we move this flag definition within dashboard
+# plugin, users will have to explicitly enable dashboard plugin
 # to also use flags provided by it.
 flags.add_argument(
     '--enable-dashboard',
@@ -89,39 +85,73 @@ flags.add_argument(
     help='Default: False.  Enables proxy.py dashboard.',
 )
 
+flags.add_argument(
+    '--work-klass',
+    type=str,
+    default=DEFAULT_WORK_KLASS,
+    help='Default: ' + DEFAULT_WORK_KLASS +
+    '.  Work klass to use for work execution.',
+)
+
 
 class Proxy:
-    """Context manager to control AcceptorPool & Eventing core lifecycle.
+    """Context manager to control AcceptorPool, ExecutorPool & EventingCore lifecycle.
 
     By default, AcceptorPool is started with `HttpProtocolHandler` work class.
     By definition, it expects HTTP traffic to flow between clients and server.
 
-    Optionally, it also initializes the eventing core, a multiprocess safe
+    Optionally, it also initializes the eventing core, a multi-process safe
     pubsub system queue which can be used to build various patterns
     for message sharing and/or signaling.
     """
 
-    def __init__(self, input_args: Optional[List[str]], **opts: Any) -> None:
+    def __init__(self, input_args: Optional[List[str]] = None, **opts: Any) -> None:
         self.flags = FlagParser.initialize(input_args, **opts)
-        self.pool: Optional[AcceptorPool] = None
-        # TODO(abhinavsingh): Allow users to override the worker class itself
-        # e.g. A clear text protocol. Or imagine a TelnetProtocolHandler instead
-        # of default HttpProtocolHandler.
-        self.work_klass: Type[Work] = HttpProtocolHandler
+        self.acceptors: Optional[AcceptorPool] = None
+        self.executors: Optional[ThreadlessPool] = None
         self.event_manager: Optional[EventManager] = None
 
     def __enter__(self) -> 'Proxy':
+        self.setup()
+        return self
+
+    def __exit__(self, *args: Any) -> None:
+        self.shutdown()
+
+    def setup(self) -> None:
+        # TODO: Introduce cron feature
+        # https://github.com/abhinavsingh/proxy.py/issues/392
+        #
+        # TODO: Introduce ability to publish
+        # adhoc events which can modify behaviour of server
+        # at runtime.  Example, updating flags, plugin
+        # configuration etc.
+        #
+        # TODO: Python shell within running proxy.py environment?
+        #
+        # TODO: Pid watcher which watches for processes started
+        # by proxy.py core.  May be alert or restart those processes
+        # on failure.
         if self.flags.enable_events:
             logger.info('Core Event enabled')
             self.event_manager = EventManager()
             self.event_manager.setup()
-        self.pool = AcceptorPool(
+        event_queue = self.event_manager.queue \
+            if self.event_manager is not None \
+            else None
+        self.executors = ThreadlessPool(
             flags=self.flags,
-            work_klass=self.work_klass,
-            event_queue=self.event_manager.queue if self.event_manager is not None else None,
+            event_queue=event_queue,
         )
-        self.pool.setup()
-        assert self.pool is not None
+        self.executors.setup()
+        self.acceptors = AcceptorPool(
+            flags=self.flags,
+            event_queue=event_queue,
+            executor_queues=self.executors.work_queues,
+            executor_pids=self.executors.work_pids,
+        )
+        self.acceptors.setup()
+        assert self.acceptors is not None
         if self.flags.unix_socket_path:
             logger.info(
                 'Listening on %s' %
@@ -130,42 +160,22 @@ class Proxy:
         else:
             logger.info(
                 'Listening on %s:%s' %
-                (self.pool.flags.hostname, self.pool.flags.port),
+                (self.acceptors.flags.hostname, self.acceptors.flags.port),
             )
-        return self
 
-    def __exit__(
-            self,
-            exc_type: Optional[Type[BaseException]],
-            exc_val: Optional[BaseException],
-            exc_tb: Optional[TracebackType],
-    ) -> None:
-        assert self.pool
-        self.pool.shutdown()
+    def shutdown(self) -> None:
+        assert self.acceptors
+        self.acceptors.shutdown()
+        assert self.executors
+        self.executors.shutdown()
         if self.flags.enable_events:
             assert self.event_manager is not None
             self.event_manager.shutdown()
 
 
-def main(
-        input_args: Optional[List[str]] = None,
-        **opts: Any,
-) -> None:
+def main(**opts: Any) -> None:
     try:
-        with Proxy(input_args=input_args, **opts):
-            # TODO: Introduce cron feature
-            # https://github.com/abhinavsingh/proxy.py/issues/392
-            #
-            # TODO: Introduce ability to publish
-            # adhoc events which can modify behaviour of server
-            # at runtime.  Example, updating flags, plugin
-            # configuration etc.
-            #
-            # TODO: Python shell within running proxy.py environment?
-            #
-            # TODO: Pid watcher which watches for processes started
-            # by proxy.py core.  May be alert or restart those processes
-            # on failure.
+        with Proxy(sys.argv[1:], **opts):
             while True:
                 time.sleep(1)
     except KeyboardInterrupt:
@@ -173,4 +183,4 @@ def main(
 
 
 def entry_point() -> None:
-    main(input_args=sys.argv[1:])
+    main()


Test patch:
diff --git a/proxy/testing/test_case.py b/proxy/testing/test_case.py
index cb33d61..2bec277 100644
--- a/proxy/testing/test_case.py
+++ b/proxy/testing/test_case.py
@@ -24,6 +24,7 @@ class TestCase(unittest.TestCase):
 
     DEFAULT_PROXY_PY_STARTUP_FLAGS = [
         '--num-workers', '1',
+        '--num-acceptors', '1',
         '--threadless',
     ]
 
@@ -40,14 +41,14 @@ class TestCase(unittest.TestCase):
         cls.INPUT_ARGS.append('--port')
         cls.INPUT_ARGS.append('0')
 
-        cls.PROXY = Proxy(input_args=cls.INPUT_ARGS)
+        cls.PROXY = Proxy(cls.INPUT_ARGS)
         cls.PROXY.flags.plugins[b'HttpProxyBasePlugin'].append(
             CacheResponsesPlugin,
         )
 
         cls.PROXY.__enter__()
-        assert cls.PROXY.pool
-        cls.wait_for_server(cls.PROXY.pool.flags.port)
+        assert cls.PROXY.acceptors
+        cls.wait_for_server(cls.PROXY.acceptors.flags.port)
 
     @staticmethod
     def wait_for_server(
diff --git a/tests/common/test_flags.py b/tests/common/test_flags.py
index 25c8212..a8791d5 100644
--- a/tests/common/test_flags.py
+++ b/tests/common/test_flags.py
@@ -8,16 +8,18 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
-from proxy.common.utils import bytes_
-from proxy.common.constants import PLUGIN_HTTP_PROXY
 import unittest
 
+from unittest import mock
 from typing import List, Dict
 
-from proxy.common.flag import FlagParser
 from proxy.http.proxy import HttpProxyPlugin
 from proxy.plugin import CacheResponsesPlugin
 from proxy.plugin import FilterByUpstreamHostPlugin
+from proxy.common.utils import bytes_
+from proxy.common.flag import FlagParser
+from proxy.common.version import __version__
+from proxy.common.constants import PLUGIN_HTTP_PROXY, PY2_DEPRECATION_MESSAGE
 
 
 class TestFlags(unittest.TestCase):
@@ -139,6 +141,43 @@ class TestFlags(unittest.TestCase):
             ],
         })
 
+    def test_basic_auth_flag_is_base64_encoded(self) -> None:
+        flags = FlagParser.initialize(['--basic-auth', 'user:pass'])
+        self.assertEqual(flags.auth_code, b'dXNlcjpwYXNz')
+
+    @mock.patch('builtins.print')
+    def test_main_version(self, mock_print: mock.Mock) -> None:
+        with self.assertRaises(SystemExit) as e:
+            FlagParser.initialize(['--version'])
+            mock_print.assert_called_with(__version__)
+        self.assertEqual(e.exception.code, 0)
+
+    @mock.patch('builtins.print')
+    @mock.patch('proxy.common.flag.is_py2')
+    def test_main_py2_exit(
+            self,
+            mock_is_py2: mock.Mock,
+            mock_print: mock.Mock,
+    ) -> None:
+        mock_is_py2.return_value = True
+        with self.assertRaises(SystemExit) as e:
+            FlagParser.initialize()
+        mock_print.assert_called_with(PY2_DEPRECATION_MESSAGE)
+        self.assertEqual(e.exception.code, 1)
+        mock_is_py2.assert_called()
+
+    @mock.patch('builtins.print')
+    @mock.patch('proxy.common.flag.is_py2')
+    def test_main_py3_runs(
+            self,
+            mock_is_py2: mock.Mock,
+            mock_print: mock.Mock,
+    ) -> None:
+        mock_is_py2.return_value = False
+        FlagParser.initialize()
+        mock_is_py2.assert_called()
+        mock_print.assert_not_called()
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/core/test_acceptor.py b/tests/core/test_acceptor.py
index 595513f..274399c 100644
--- a/tests/core/test_acceptor.py
+++ b/tests/core/test_acceptor.py
@@ -22,15 +22,17 @@ class TestAcceptor(unittest.TestCase):
 
     def setUp(self) -> None:
         self.acceptor_id = 1
-        self.mock_protocol_handler = mock.MagicMock()
         self.pipe = multiprocessing.Pipe()
-        self.flags = FlagParser.initialize(threaded=True)
+        self.flags = FlagParser.initialize(
+            threaded=True, work_klass=mock.MagicMock(),
+        )
         self.acceptor = Acceptor(
             idd=self.acceptor_id,
             work_queue=self.pipe[1],
             flags=self.flags,
             lock=multiprocessing.Lock(),
-            work_klass=self.mock_protocol_handler,
+            executor_queues=[],
+            executor_pids=[],
         )
 
     @mock.patch('selectors.DefaultSelector')
@@ -55,9 +57,9 @@ class TestAcceptor(unittest.TestCase):
         self.acceptor.run()
 
         sock.accept.assert_not_called()
-        self.mock_protocol_handler.assert_not_called()
+        self.flags.work_klass.assert_not_called()
 
-    @mock.patch('proxy.core.acceptor.acceptor.TcpClientConnection')
+    @mock.patch('proxy.core.acceptor.executors.TcpClientConnection')
     @mock.patch('threading.Thread')
     @mock.patch('selectors.DefaultSelector')
     @mock.patch('socket.fromfd')
@@ -92,13 +94,13 @@ class TestAcceptor(unittest.TestCase):
             family=socket.AF_INET6,
             type=socket.SOCK_STREAM,
         )
-        self.mock_protocol_handler.assert_called_with(
+        self.flags.work_klass.assert_called_with(
             mock_client.return_value,
             flags=self.flags,
             event_queue=None,
         )
         mock_thread.assert_called_with(
-            target=self.mock_protocol_handler.return_value.run,
+            target=self.flags.work_klass.return_value.run,
         )
         mock_thread.return_value.start.assert_called()
         sock.close.assert_called()
diff --git a/tests/core/test_acceptor_pool.py b/tests/core/test_acceptor_pool.py
index 66a910f..e2b1064 100644
--- a/tests/core/test_acceptor_pool.py
+++ b/tests/core/test_acceptor_pool.py
@@ -46,16 +46,14 @@ class TestAcceptorPool(unittest.TestCase):
         num_workers = 2
         pid_file = os.path.join(tempfile.gettempdir(), 'pid')
         sock = mock_socket.return_value
-        work_klass = mock.MagicMock()
         flags = FlagParser.initialize(
             num_workers=2, pid_file=pid_file, threaded=True,
         )
 
-        pool = AcceptorPool(flags=flags, work_klass=work_klass)
+        pool = AcceptorPool(flags=flags, executor_queues=[], executor_pids=[])
         pool.setup()
         mock_send_handle.assert_called()
 
-        work_klass.assert_not_called()
         mock_socket.assert_called_with(
             socket.AF_INET6 if pool.flags.hostname.version == 6 else socket.AF_INET,
             socket.SOCK_STREAM,
diff --git a/tests/http/exceptions/test_http_proxy_auth_failed.py b/tests/http/exceptions/test_http_proxy_auth_failed.py
index c1d369a..a65fc04 100644
--- a/tests/http/exceptions/test_http_proxy_auth_failed.py
+++ b/tests/http/exceptions/test_http_proxy_auth_failed.py
@@ -14,7 +14,7 @@ from unittest import mock
 
 from proxy.common.flag import FlagParser
 from proxy.http.exception.proxy_auth_failed import ProxyAuthenticationFailed
-from proxy.http.handler import HttpProtocolHandler
+from proxy.http import HttpProtocolHandler
 from proxy.core.connection import TcpClientConnection
 from proxy.common.utils import build_http_request
 
diff --git a/tests/http/test_http_proxy.py b/tests/http/test_http_proxy.py
index f658de8..fde5da1 100644
--- a/tests/http/test_http_proxy.py
+++ b/tests/http/test_http_proxy.py
@@ -16,7 +16,7 @@ from proxy.common.constants import DEFAULT_HTTP_PORT
 from proxy.common.flag import FlagParser
 from proxy.core.connection import TcpClientConnection
 from proxy.http.proxy import HttpProxyPlugin
-from proxy.http.handler import HttpProtocolHandler
+from proxy.http import HttpProtocolHandler
 from proxy.http.exception import HttpProtocolException
 from proxy.common.utils import build_http_request
 
diff --git a/tests/http/test_http_proxy_tls_interception.py b/tests/http/test_http_proxy_tls_interception.py
index 987d5c5..0b13aa8 100644
--- a/tests/http/test_http_proxy_tls_interception.py
+++ b/tests/http/test_http_proxy_tls_interception.py
@@ -19,7 +19,7 @@ from unittest import mock
 from proxy.common.constants import DEFAULT_CA_FILE
 
 from proxy.core.connection import TcpClientConnection, TcpServerConnection
-from proxy.http.handler import HttpProtocolHandler
+from proxy.http import HttpProtocolHandler
 from proxy.http.proxy import HttpProxyPlugin
 from proxy.http.methods import httpMethods
 from proxy.common.utils import build_http_request, bytes_
diff --git a/tests/http/test_protocol_handler.py b/tests/http/test_protocol_handler.py
index 14a1f5a..7ef30b7 100644
--- a/tests/http/test_protocol_handler.py
+++ b/tests/http/test_protocol_handler.py
@@ -25,7 +25,7 @@ from proxy.http.parser import HttpParser
 from proxy.http.proxy import HttpProxyPlugin
 from proxy.http.parser import httpParserStates, httpParserTypes
 from proxy.http.exception import ProxyAuthenticationFailed, ProxyConnectionFailed
-from proxy.http.handler import HttpProtocolHandler
+from proxy.http import HttpProtocolHandler
 
 
 class TestHttpProtocolHandler(unittest.TestCase):
diff --git a/tests/http/test_web_server.py b/tests/http/test_web_server.py
index 6ef6948..be00981 100644
--- a/tests/http/test_web_server.py
+++ b/tests/http/test_web_server.py
@@ -18,7 +18,7 @@ from unittest import mock
 from proxy.common.plugins import Plugins
 from proxy.common.flag import FlagParser
 from proxy.core.connection import TcpClientConnection
-from proxy.http.handler import HttpProtocolHandler
+from proxy.http import HttpProtocolHandler
 from proxy.http.parser import HttpParser, httpParserStates, httpParserTypes
 from proxy.common.utils import build_http_response, build_http_request, bytes_, text_
 from proxy.common.constants import CRLF, PLUGIN_HTTP_PROXY, PLUGIN_PAC_FILE, PLUGIN_WEB_SERVER, PROXY_PY_DIR
diff --git a/tests/plugin/test_http_proxy_plugins.py b/tests/plugin/test_http_proxy_plugins.py
index 36bbbcb..97dae26 100644
--- a/tests/plugin/test_http_proxy_plugins.py
+++ b/tests/plugin/test_http_proxy_plugins.py
@@ -19,7 +19,7 @@ from pathlib import Path
 
 from proxy.common.flag import FlagParser
 from proxy.core.connection import TcpClientConnection
-from proxy.http.handler import HttpProtocolHandler
+from proxy.http import HttpProtocolHandler
 from proxy.http.proxy import HttpProxyPlugin
 from proxy.common.utils import build_http_request, bytes_, build_http_response
 from proxy.common.constants import PROXY_AGENT_HEADER_VALUE, DEFAULT_HTTP_PORT
diff --git a/tests/plugin/test_http_proxy_plugins_with_tls_interception.py b/tests/plugin/test_http_proxy_plugins_with_tls_interception.py
index 42dc9e4..6415755 100644
--- a/tests/plugin/test_http_proxy_plugins_with_tls_interception.py
+++ b/tests/plugin/test_http_proxy_plugins_with_tls_interception.py
@@ -21,7 +21,7 @@ from proxy.common.utils import bytes_, build_http_request, build_http_response
 from proxy.core.connection import TcpClientConnection, TcpServerConnection
 from proxy.http.codes import httpStatusCodes
 from proxy.http.methods import httpMethods
-from proxy.http.handler import HttpProtocolHandler
+from proxy.http import HttpProtocolHandler
 from proxy.http.proxy import HttpProxyPlugin
 
 from .utils import get_plugin_by_test_name
diff --git a/tests/test_main.py b/tests/test_main.py
index 315652b..21137b6 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -14,8 +14,6 @@ from unittest import mock
 
 from proxy.proxy import main, entry_point
 from proxy.common.utils import bytes_
-from proxy.http.handler import HttpProtocolHandler
-from proxy.common.flag import FlagParser
 
 from proxy.common.constants import DEFAULT_ENABLE_DASHBOARD, DEFAULT_LOG_LEVEL, DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT
 from proxy.common.constants import DEFAULT_TIMEOUT, DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HTTP_PROXY
@@ -24,16 +22,16 @@ from proxy.common.constants import DEFAULT_ENABLE_WEB_SERVER, DEFAULT_THREADLESS
 from proxy.common.constants import DEFAULT_CA_CERT_FILE, DEFAULT_CA_KEY_FILE, DEFAULT_CA_SIGNING_KEY_FILE
 from proxy.common.constants import DEFAULT_PAC_FILE, DEFAULT_PLUGINS, DEFAULT_PID_FILE, DEFAULT_PORT, DEFAULT_BASIC_AUTH
 from proxy.common.constants import DEFAULT_NUM_WORKERS, DEFAULT_OPEN_FILE_LIMIT, DEFAULT_IPV6_HOSTNAME
-from proxy.common.constants import DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CLIENT_RECVBUF_SIZE, PY2_DEPRECATION_MESSAGE
+from proxy.common.constants import DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CLIENT_RECVBUF_SIZE, DEFAULT_WORK_KLASS
 from proxy.common.constants import PLUGIN_INSPECT_TRAFFIC, PLUGIN_DASHBOARD, PLUGIN_DEVTOOLS_PROTOCOL, PLUGIN_WEB_SERVER
-from proxy.common.constants import PLUGIN_HTTP_PROXY
-from proxy.common.version import __version__
+from proxy.common.constants import PLUGIN_HTTP_PROXY, DEFAULT_NUM_ACCEPTORS
 
 
 class TestMain(unittest.TestCase):
 
     @staticmethod
     def mock_default_args(mock_args: mock.Mock) -> None:
+        """Use when trying to mock parse_args"""
         mock_args.version = False
         mock_args.cert_file = DEFAULT_CERT_FILE
         mock_args.key_file = DEFAULT_KEY_FILE
@@ -47,6 +45,7 @@ class TestMain(unittest.TestCase):
         mock_args.basic_auth = DEFAULT_BASIC_AUTH
         mock_args.hostname = DEFAULT_IPV6_HOSTNAME
         mock_args.port = DEFAULT_PORT
+        mock_args.num_acceptors = DEFAULT_NUM_ACCEPTORS
         mock_args.num_workers = DEFAULT_NUM_WORKERS
         mock_args.disable_http_proxy = DEFAULT_DISABLE_HTTP_PROXY
         mock_args.pac_file = DEFAULT_PAC_FILE
@@ -64,13 +63,16 @@ class TestMain(unittest.TestCase):
         mock_args.enable_devtools = DEFAULT_ENABLE_DEVTOOLS
         mock_args.enable_events = DEFAULT_ENABLE_EVENTS
         mock_args.enable_dashboard = DEFAULT_ENABLE_DASHBOARD
+        mock_args.work_klass = DEFAULT_WORK_KLASS
 
     @mock.patch('time.sleep')
     @mock.patch('proxy.proxy.FlagParser.initialize')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
+    @mock.patch('proxy.proxy.ThreadlessPool')
     def test_entry_point(
             self,
+            mock_executor_pool: mock.Mock,
             mock_acceptor_pool: mock.Mock,
             mock_event_manager: mock.Mock,
             mock_initialize: mock.Mock,
@@ -80,11 +82,16 @@ class TestMain(unittest.TestCase):
         mock_initialize.return_value.enable_events = False
         entry_point()
         mock_event_manager.assert_not_called()
-        mock_acceptor_pool.assert_called_with(
+        mock_executor_pool.assert_called_once_with(
             flags=mock_initialize.return_value,
-            work_klass=HttpProtocolHandler,
             event_queue=None,
         )
+        mock_acceptor_pool.assert_called_once_with(
+            flags=mock_initialize.return_value,
+            event_queue=None,
+            executor_queues=mock_executor_pool.return_value.work_queues,
+            executor_pids=mock_executor_pool.return_value.work_pids,
+        )
         mock_acceptor_pool.return_value.setup.assert_called()
         mock_acceptor_pool.return_value.shutdown.assert_called()
         mock_sleep.assert_called()
@@ -93,8 +100,10 @@ class TestMain(unittest.TestCase):
     @mock.patch('proxy.proxy.FlagParser.initialize')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
-    def test_main_with_no_arguments(
+    @mock.patch('proxy.proxy.ThreadlessPool')
+    def test_main_with_no_flags(
             self,
+            mock_executor_pool: mock.Mock,
             mock_acceptor_pool: mock.Mock,
             mock_event_manager: mock.Mock,
             mock_initialize: mock.Mock,
@@ -102,12 +111,17 @@ class TestMain(unittest.TestCase):
     ) -> None:
         mock_sleep.side_effect = KeyboardInterrupt()
         mock_initialize.return_value.enable_events = False
-        main([])
+        main()
         mock_event_manager.assert_not_called()
-        mock_acceptor_pool.assert_called_with(
+        mock_executor_pool.assert_called_once_with(
+            flags=mock_initialize.return_value,
+            event_queue=None,
+        )
+        mock_acceptor_pool.assert_called_once_with(
             flags=mock_initialize.return_value,
-            work_klass=HttpProtocolHandler,
             event_queue=None,
+            executor_queues=mock_executor_pool.return_value.work_queues,
+            executor_pids=mock_executor_pool.return_value.work_pids,
         )
         mock_acceptor_pool.return_value.setup.assert_called()
         mock_acceptor_pool.return_value.shutdown.assert_called()
@@ -117,23 +131,30 @@ class TestMain(unittest.TestCase):
     @mock.patch('proxy.proxy.FlagParser.initialize')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
+    @mock.patch('proxy.proxy.ThreadlessPool')
     def test_enable_events(
-        self,
-        mock_acceptor_pool: mock.Mock,
-        mock_event_manager: mock.Mock,
-        mock_initialize: mock.Mock,
-        mock_sleep: mock.Mock,
+            self,
+            mock_executor_pool: mock.Mock,
+            mock_acceptor_pool: mock.Mock,
+            mock_event_manager: mock.Mock,
+            mock_initialize: mock.Mock,
+            mock_sleep: mock.Mock,
     ) -> None:
         mock_sleep.side_effect = KeyboardInterrupt()
         mock_initialize.return_value.enable_events = True
-        main([])
+        main()
         mock_event_manager.assert_called_once()
         mock_event_manager.return_value.setup.assert_called_once()
         mock_event_manager.return_value.shutdown.assert_called_once()
-        mock_acceptor_pool.assert_called_with(
+        mock_executor_pool.assert_called_once_with(
+            flags=mock_initialize.return_value,
+            event_queue=mock_event_manager.return_value.queue,
+        )
+        mock_acceptor_pool.assert_called_once_with(
             flags=mock_initialize.return_value,
-            work_klass=HttpProtocolHandler,
             event_queue=mock_event_manager.return_value.queue,
+            executor_queues=mock_executor_pool.return_value.work_queues,
+            executor_pids=mock_executor_pool.return_value.work_pids,
         )
         mock_acceptor_pool.return_value.setup.assert_called()
         mock_acceptor_pool.return_value.shutdown.assert_called()
@@ -144,19 +165,21 @@ class TestMain(unittest.TestCase):
     @mock.patch('proxy.common.flag.FlagParser.parse_args')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
+    @mock.patch('proxy.proxy.ThreadlessPool')
     def test_enable_dashboard(
-        self,
-        mock_acceptor_pool: mock.Mock,
-        mock_event_manager: mock.Mock,
-        mock_parse_args: mock.Mock,
-        mock_load_plugins: mock.Mock,
-        mock_sleep: mock.Mock,
+            self,
+            mock_executor_pool: mock.Mock,
+            mock_acceptor_pool: mock.Mock,
+            mock_event_manager: mock.Mock,
+            mock_parse_args: mock.Mock,
+            mock_load_plugins: mock.Mock,
+            mock_sleep: mock.Mock,
     ) -> None:
         mock_sleep.side_effect = KeyboardInterrupt()
         mock_args = mock_parse_args.return_value
         self.mock_default_args(mock_args)
         mock_args.enable_dashboard = True
-        main(['--enable-dashboard'])
+        main(enable_dashboard=True)
         mock_load_plugins.assert_called()
         self.assertEqual(
             mock_load_plugins.call_args_list[0][0][0], [
@@ -167,32 +190,37 @@ class TestMain(unittest.TestCase):
                 bytes_(PLUGIN_HTTP_PROXY),
             ],
         )
+        # TODO: Assert arguments passed to parse_arg
         mock_parse_args.assert_called_once()
-        mock_acceptor_pool.assert_called()
-        mock_acceptor_pool.return_value.setup.assert_called()
         # dashboard will also enable eventing
         mock_event_manager.assert_called_once()
         mock_event_manager.return_value.setup.assert_called_once()
         mock_event_manager.return_value.shutdown.assert_called_once()
+        mock_executor_pool.assert_called_once()
+        mock_executor_pool.return_value.setup.assert_called_once()
+        mock_acceptor_pool.assert_called_once()
+        mock_acceptor_pool.return_value.setup.assert_called_once()
 
     @mock.patch('time.sleep')
     @mock.patch('proxy.common.plugins.Plugins.load')
     @mock.patch('proxy.common.flag.FlagParser.parse_args')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
+    @mock.patch('proxy.proxy.ThreadlessPool')
     def test_enable_devtools(
-        self,
-        mock_acceptor_pool: mock.Mock,
-        mock_event_manager: mock.Mock,
-        mock_parse_args: mock.Mock,
-        mock_load_plugins: mock.Mock,
-        mock_sleep: mock.Mock,
+            self,
+            mock_executor_pool: mock.Mock,
+            mock_acceptor_pool: mock.Mock,
+            mock_event_manager: mock.Mock,
+            mock_parse_args: mock.Mock,
+            mock_load_plugins: mock.Mock,
+            mock_sleep: mock.Mock,
     ) -> None:
         mock_sleep.side_effect = KeyboardInterrupt()
         mock_args = mock_parse_args.return_value
         self.mock_default_args(mock_args)
         mock_args.enable_devtools = True
-        main(['--enable-devtools'])
+        main(enable_devtools=True)
         mock_load_plugins.assert_called()
         self.assertEqual(
             mock_load_plugins.call_args_list[0][0][0], [
@@ -202,83 +230,12 @@ class TestMain(unittest.TestCase):
             ],
         )
         mock_parse_args.assert_called_once()
-        mock_acceptor_pool.assert_called()
-        mock_acceptor_pool.return_value.setup.assert_called()
-        # Currently --enable-devtools alone doesn't enable eventing core
-        mock_event_manager.assert_not_called()
-
-    @mock.patch('time.sleep')
-    @mock.patch('proxy.proxy.EventManager')
-    @mock.patch('proxy.proxy.AcceptorPool')
-    def test_basic_auth_flag_is_base64_encoded(
-            self,
-            mock_acceptor_pool: mock.Mock,
-            mock_event_manager: mock.Mock,
-            mock_sleep: mock.Mock,
-    ) -> None:
-        mock_sleep.side_effect = KeyboardInterrupt()
-
-        input_args = ['--basic-auth', 'user:pass']
-        flgs = FlagParser.initialize(input_args)
-
-        main(input_args=input_args)
+        # Currently --enable-devtools flag alone doesn't enable eventing core
         mock_event_manager.assert_not_called()
+        mock_executor_pool.assert_called_once()
+        mock_executor_pool.return_value.setup.assert_called_once()
         mock_acceptor_pool.assert_called_once()
-        self.assertEqual(
-            flgs.auth_code,
-            b'dXNlcjpwYXNz',
-        )
-
-    @mock.patch('time.sleep')
-    @mock.patch('builtins.print')
-    @mock.patch('proxy.proxy.EventManager')
-    @mock.patch('proxy.proxy.AcceptorPool')
-    @mock.patch('proxy.common.flag.is_py2')
-    def test_main_py3_runs(
-            self,
-            mock_is_py2: mock.Mock,
-            mock_acceptor_pool: mock.Mock,
-            mock_event_manager: mock.Mock,
-            mock_print: mock.Mock,
-            mock_sleep: mock.Mock,
-    ) -> None:
-        mock_sleep.side_effect = KeyboardInterrupt()
-
-        input_args = ['--basic-auth', 'user:pass']
-        mock_is_py2.return_value = False
-
-        main(input_args, num_workers=1)
-
-        mock_is_py2.assert_called()
-        mock_print.assert_not_called()
-
-        mock_event_manager.assert_not_called()
-        mock_acceptor_pool.assert_called_once()
-        mock_acceptor_pool.return_value.setup.assert_called()
-
-    @mock.patch('builtins.print')
-    @mock.patch('proxy.common.flag.is_py2')
-    def test_main_py2_exit(
-            self,
-            mock_is_py2: mock.Mock,
-            mock_print: mock.Mock,
-    ) -> None:
-        mock_is_py2.return_value = True
-        with self.assertRaises(SystemExit) as e:
-            main(num_workers=1)
-        mock_print.assert_called_with(PY2_DEPRECATION_MESSAGE)
-        self.assertEqual(e.exception.code, 1)
-        mock_is_py2.assert_called()
-
-    @mock.patch('builtins.print')
-    def test_main_version(
-            self,
-            mock_print: mock.Mock,
-    ) -> None:
-        with self.assertRaises(SystemExit) as e:
-            main(['--version'])
-            mock_print.assert_called_with(__version__)
-        self.assertEqual(e.exception.code, 0)
+        mock_acceptor_pool.return_value.setup.assert_called_once()
 
     # def test_pac_file(self) -> None:
     #     pass
diff --git a/tests/testing/test_embed.py b/tests/testing/test_embed.py
index 2f9f7d0..7fcd169 100644
--- a/tests/testing/test_embed.py
+++ b/tests/testing/test_embed.py
@@ -34,13 +34,13 @@ class TestProxyPyEmbedded(TestCase):
 
     def test_with_proxy(self) -> None:
         """Makes a HTTP request to in-build web server via proxy server."""
-        assert self.PROXY and self.PROXY.pool
-        with socket_connection(('localhost', self.PROXY.pool.flags.port)) as conn:
+        assert self.PROXY and self.PROXY.acceptors
+        with socket_connection(('localhost', self.PROXY.acceptors.flags.port)) as conn:
             conn.send(
                 build_http_request(
-                    httpMethods.GET, b'http://localhost:%d/' % self.PROXY.pool.flags.port,
+                    httpMethods.GET, b'http://localhost:%d/' % self.PROXY.acceptors.flags.port,
                     headers={
-                        b'Host': b'localhost:%d' % self.PROXY.pool.flags.port,
+                        b'Host': b'localhost:%d' % self.PROXY.acceptors.flags.port,
                     },
                 ),
             )
@@ -73,15 +73,15 @@ class TestProxyPyEmbedded(TestCase):
         self.make_http_request_using_proxy()
 
     def make_http_request_using_proxy(self) -> None:
-        assert self.PROXY and self.PROXY.pool
+        assert self.PROXY and self.PROXY.acceptors
         proxy_handler = urllib.request.ProxyHandler({
-            'http': 'http://localhost:%d' % self.PROXY.pool.flags.port,
+            'http': 'http://localhost:%d' % self.PROXY.acceptors.flags.port,
         })
         opener = urllib.request.build_opener(proxy_handler)
         with self.assertRaises(urllib.error.HTTPError):
             r: http.client.HTTPResponse = opener.open(
                 'http://localhost:%d/' %
-                self.PROXY.pool.flags.port, timeout=10,
+                self.PROXY.acceptors.flags.port, timeout=10,
             )
             self.assertEqual(r.status, 404)
             self.assertEqual(r.headers.get('server'), PROXY_AGENT_HEADER_VALUE)