# sync_worker

URL: /pounce/api/sync_worker/
Section: api
Description: SyncWorker — blocking I/O worker for request-response workloads.

One request at a time per thread, no asyncio. On 3.14t, runs in a thread
with true parallelism. Handles HTTP/1.1 keep-alive in a tight recv/send loop.

When the ASGI app returns a streaming response (more_body=True) or WebSocket
upgrade, raises NeedsAsyncError — the supervisor hands off to the async pool
(Phase 2). For Phase 1, streaming requests receive 501 Not Implemented.

---

> For a complete page index, fetch /pounce/llms.txt.

Open LLM text
(/pounce/api/sync_worker/index.txt)

Share with AI

Ask Claude
(https://claude.ai/new?q=Please%20help%20me%20understand%20this%20documentation%3A%20%2Fpounce%2Fapi%2Fsync_worker%2Findex.txt)

Ask ChatGPT
(https://chatgpt.com/?q=Please%20help%20me%20understand%20this%20documentation%3A%20%2Fpounce%2Fapi%2Fsync_worker%2Findex.txt)

Ask Gemini
(https://gemini.google.com/app?q=Please%20help%20me%20understand%20this%20documentation%3A%20%2Fpounce%2Fapi%2Fsync_worker%2Findex.txt)

Ask Copilot
(https://copilot.microsoft.com/?q=Please%20help%20me%20understand%20this%20documentation%3A%20%2Fpounce%2Fapi%2Fsync_worker%2Findex.txt)

Module

#
`sync_worker`

SyncWorker — blocking I/O worker for request-response workloads.

One request at a time per thread, no asyncio. On 3.14t, runs in a thread
with true parallelism. Handles HTTP/1.1 keep-alive in a tight recv/send loop.

When the ASGI app returns a streaming response (more_body=True) or WebSocket
upgrade, raises NeedsAsyncError — the supervisor hands off to the async pool
(Phase 2). For Phase 1, streaming requests receive 501 Not Implemented.

2Classes6Functions

## Classes

`_RequestMeta`

1

▼

Pre-extracted header values from a single pass over request headers.

All fields populated by ``_cl…

Pre-extracted header values from a single pass over request headers.

All fields populated by `_classify_request()` (/pounce/api/sync_worker/#_classify_request) — avoids redundant
linear scans per request. Header names from `_fast_h1` (/pounce/api/_fast_h1/) are already
lowered, so no`.lower()`is needed on names.

#### Methods

Internal Methods
1

▼

`__init__`

0

▼

`def __init__(self) -> None`

`SyncWorker`

24

▼

Blocking I/O worker — one request at a time, no event loop.

On 3.14t, runs in a thread with true p…

Blocking I/O worker — one request at a time, no event loop.

On 3.14t, runs in a thread with true parallelism. Handles HTTP/1.1
keep-alive in a tight recv/send loop. Streaming and WebSocket require
handoff to the async pool (Phase 2).

#### Methods

`set_lifespan_state`

1

▼

Set the lifespan state dict shared with all requests.

`def set_lifespan_state(self, state: dict[str, Any]) -> None`

##### Parameters

Name
Type
Description

`state`
`—`

`start_draining`

0

▼

Mark this worker as draining (stop accepting new connections).

`def start_draining(self) -> None`

`is_idle`

0

`bool`

▼

True if no connection is currently being handled.

`def is_idle(self) -> bool`

##### Returns

`bool`

`run`

0

▼

Accept connections until shutdown (blocking).

`def run(self) -> None`

Internal Methods
20

▼

`__init__`

12

▼

`def __init__(self, config: ServerConfig, app: ASGIApp, sock: socket.socket | None, *, worker_id: int = 0, generation: int = 0, shutdown_event: threading.Event | None = None, ssl_context: ssl.SSLContext | None = None, lifecycle_collector: LifecycleCollector | None = None, async_pool: AsyncPool | None = None, conn_queue: queue.Queue[tuple[socket.socket, object]] | None = None, sync_app: SyncApp | None = None, startup_status_queue: Any | None = None) -> None`

##### Parameters

Name
Type
Description

`config`
`—`

`app`
`—`

`sock`
`—`

`worker_id`
`—`

Default:`0`

`generation`
`—`

Default:`0`

`shutdown_event`
`—`

Default:`None`

`ssl_context`
`—`

Default:`None`

`lifecycle_collector`
`—`

Default:`None`

`async_pool`
`—`

Default:`None`

`conn_queue`
`—`

Default:`None`

`sync_app`
`—`

Default:`None`

`startup_status_queue`
`—`

Default:`None`

`_active_connection_count`

0

`int`

▼

Return the active connection count under its free-threading lock.

`def _active_connection_count(self) -> int`

##### Returns

`int`

`_report_startup_status`

2

▼

Report initial startup state to the owning supervisor.

`def _report_startup_status(self, status: str, detail: str | None = None) -> None`

##### Parameters

Name
Type
Description

`status`
`—`

`detail`
`—`

Default:`None`

`_run_worker_startup_hook`

0

`bool`

▼

Run ``pounce.worker.startup`` on this sync worker's runner loop.

async

`async def _run_worker_startup_hook(self) -> bool`

##### Returns

`bool`

`_run_worker_shutdown_hook`

0

▼

Run ``pounce.worker.shutdown`` on this sync worker's runner loop.

async

`async def _run_worker_shutdown_hook(self) -> None`

`_run_worker_draining_hook`

1

▼

Notify the app before streaming handoffs are retired.

async

`async def _run_worker_draining_hook(self, reason: str) -> None`

##### Parameters

Name
Type
Description

`reason`
`—`

`_run_from_queue`

2

▼

Get connections from distributor queue (no thundering herd).

`def _run_from_queue(self, poll_interval: float, runner: asyncio.Runner) -> None`

##### Parameters

Name
Type
Description

`poll_interval`
`—`

`runner`
`—`

`_run_accept_loop`

2

▼

Accept directly from socket (SO_REUSEPORT or single worker).

`def _run_accept_loop(self, poll_interval: float, runner: asyncio.Runner) -> None`

##### Parameters

Name
Type
Description

`poll_interval`
`—`

`runner`
`—`

`_drain_pending_queue`

1

▼

Serve in-flight queued connections on a FULL shutdown; never reset them.

Only …

`def _drain_pending_queue(self, runner: asyncio.Runner) -> None`

Serve in-flight queued connections on a FULL shutdown; never reset them.

Only acts on a full shutdown (`_ext_shutdown`set). On a graceful
reload (`_drain_event` set, `_ext_shutdown`unset) the shared
`conn_queue`is still owned by the AcceptDistributor, which keeps
enqueuing for the NEW generation — its own`drain_event`is only set
on full shutdown, not reload. Touching those entries here would steal
connections the new generation is meant to serve, so leave them
untouched and let this retiring worker simply exit (issue #102).

Every connection left in the queue was accepted by the distributor
before drain began (the distributor 503s anything it accepts after),
so each holds an in-flight request the client is waiting on. Closing
such a socket — even after a 503 — RSTs it because the client's unread
request bytes are still in the receive buffer, surfacing as a
`ConnectionResetError`for an in-flight request (#104). So SERVE each
queued connection to completion instead.`_handle_connection`already
forces`Connection: close`while draining, so each finishes one
request and closes cleanly. The whole drain-serve loop is bounded by
`shutdown_timeout`; any connection still queued past the deadline gets
a bounded 503 so the worker thread can still exit promptly.

##### Parameters

Name
Type
Description

`runner`
`—`

`_drain_accept_window`

0

▼

For a bounded window, accept new connections and answer them a 503.

Only runs …

`def _drain_accept_window(self) -> None`

For a bounded window, accept new connections and answer them a 503.

Only runs once the accept loop is stopping. The window is bounded by
`shutdown_timeout`so a draining worker still answers late arrivals
with an actionable refusal without blocking process exit.

This window ONLY runs on a full shutdown (SIGTERM —`_ext_shutdown`
set), where the server is going away and a brand-new connection should
get a bounded clean 503 (issue #101). On a graceful reload (SIGHUP —
`_drain_event` set but `_ext_shutdown`NOT set) the new generation
owns this listener and serves new connections, so the retiring worker
must return early: it must neither steal connections from the new gen
nor spin for the full`shutdown_timeout`refusing them (issue #102).

`_send_drain_503`

2

▼

Write the request-aware drain 503 to *conn* and close it.

`def _send_drain_503(self, conn: socket.socket, *, timeout: float | None = None) -> None`

##### Parameters

Name
Type
Description

`conn`
`—`

`timeout`
`—`

Default:`None`

`_handle_connection`

3

▼

Handle a single TCP connection through request-response cycles.

`def _handle_connection(self, conn: socket.socket, addr: tuple[str, int], runner: asyncio.Runner) -> None`

##### Parameters

Name
Type
Description

`conn`
`—`

`addr`
`—`

`runner`
`—`

`_handle_connection_impl`

3

`bool`

▼

Inner connection handling. Returns True if handed off to async pool.

`def _handle_connection_impl(self, conn: socket.socket, addr: tuple[str, int], runner: asyncio.Runner) -> bool`

##### Parameters

Name
Type
Description

`conn`
`—`

`addr`
`—`

`runner`
`—`

##### Returns

`bool`

`_streaming_handoff`

5

`bool`

▼

Hand a streaming response off to the async pool, or send 501.

Returns True whe…

`def _streaming_handoff(self, conn: socket.socket, scope: dict[str, Any], body: bytes, request_id: str | None, connection_id: int) -> bool`

Hand a streaming response off to the async pool, or send 501.

Returns True when the connection was handed off (caller should return
True), False when no pool is configured and a 501 was sent (caller
should break the keep-alive loop).

##### Parameters

Name
Type
Description

`conn`
`—`

`scope`
`—`

`body`
`—`

`request_id`
`—`

`connection_id`
`—`

##### Returns

`bool`

`_open_connection`

1

`tuple[tuple[str, int], t…`

▼

Resolve peer/local addresses and record ConnectionOpened.

Returns (client, ser…

`def _open_connection(self, conn: socket.socket) -> tuple[tuple[str, int], tuple[str, int], str, int, int]`

Resolve peer/local addresses and record ConnectionOpened.

Returns (client, server, client_str, conn_id, conn_start).

##### Parameters

Name
Type
Description

`conn`
`—`

##### Returns

`tuple[tuple[str, int], tuple[str, int], str, int, int]`

`_serve_builtin`

10

▼

Serialize and send a protocol-neutral built-in response.

`def _serve_builtin(self, conn: socket.socket, path: str, conn_header: bytes, response: BuiltinResponse, *, request_method: bytes, conn_id: int, request_start: int, http_version: str, client_str: str, request_id: str | None) -> None`

##### Parameters

Name
Type
Description

`conn`
`—`

`path`
`—`

`conn_header`
`—`

`response`
`—`

`request_method`
`—`

`conn_id`
`—`

`request_start`
`—`

`http_version`
`—`

`client_str`
`—`

`request_id`
`—`

`_serialize_send_and_log`

12

▼

Serialize, send, and record/log a single buffered response.

Local helper for t…

`def _serialize_send_and_log(self, conn: socket.socket, status: int, headers: list[tuple[bytes, bytes]], body_out: bytes, *, conn_id: int, request_start: int, http_version: str, record_method: str, log_method: str, log_path: str, client_str: str, request_id: str | None = None) -> None`

Serialize, send, and record/log a single buffered response.

Local helper for the SyncApp fast path and the inline ASGI path — both
bypass h11 and share the identical serialize -> sendmsg(/sendall
fallback) -> ResponseCompleted -> access-log sequence. Health and
dictionary responses keep their own plain`sendall`path.

##### Parameters

Name
Type
Description

`conn`
`—`

`status`
`—`

`headers`
`—`

`body_out`
`—`

`conn_id`
`—`

`request_start`
`—`

`http_version`
`—`

`record_method`
`—`

`log_method`
`—`

`log_path`
`—`

`client_str`
`—`

`request_id`
`—`

Default:`None`

`_recv_request_fast`

1

`tuple[RequestReceived (/pounce/api/protocols/_base/#RequestReceived) | …`

▼

Read a complete HTTP request using the fast parser.

Accumulates data in the re…

`def _recv_request_fast(self, conn: socket.socket) -> tuple[RequestReceived | None, bytes]`

Read a complete HTTP request using the fast parser.

Accumulates data in the recv buffer until a complete request
(headers + body based on Content-Length) is available.
Returns (request, body) or (None, b"") on connection close/error.

##### Parameters

Name
Type
Description

`conn`
`—`

##### Returns

`tuple[RequestReceived (/pounce/api/protocols/_base/#RequestReceived) | None, bytes]`

`_cached_date_header`

0

`bytes | None`

▼

Return cached Date header bytes, refreshing at most once per second.

`def _cached_date_header(self) -> bytes | None`

##### Returns

`bytes | None`

`_send_error`

5

▼

Send a plain-text error response (raw bytes, no h11).

When *code* is set, adds…

`def _send_error(self, conn: socket.socket, status: int, message: str, *, code: str | None = None, hint: str | None = None) -> None`

Send a plain-text error response (raw bytes, no h11).

When code is set, adds the`X-Pounce-Error-Code`response header.
When`config.debug`is True, the code and hint are appended to the
body to aid debugging.

##### Parameters

Name
Type
Description

`conn`
`—`

`status`
`—`

`message`
`—`

`code`
`—`

Default:`None`

`hint`
`—`

Default:`None`

## Functions

`_send_response_parts`

3

`None`

▼

Send an HTTP response iovec completely.

``socket.sendmsg`` may accept only a p…

`def _send_response_parts(conn: socket.socket, head: bytes, body: bytes) -> None`

Send an HTTP response iovec completely.

`socket.sendmsg`may accept only a prefix of the supplied buffers under
downstream backpressure. Preserve the allocation-free one-call fast path,
then use`sendall`only for the unsent suffix.

##### Parameters

Name
Type
Description

`conn`
`socket.socket`

`head`
`bytes`

`body`
`bytes`

`_worker_lifecycle_receive`

0

`dict[str, Any]`

▼

Return a disconnect message for worker lifecycle extension scopes.

async

`async def _worker_lifecycle_receive() -> dict[str, Any]`

##### Returns

`dict[str, Any]`

`_worker_lifecycle_send`

1

`None`

▼

Ignore messages sent by worker lifecycle extension handlers.

async

`async def _worker_lifecycle_send(message: dict[str, Any]) -> None`

##### Parameters

Name
Type
Description

`message`
`dict[str, Any]`

`_wants_100_continue`

1

`bool`

▼

Return True if the header block declares ``Expect: 100-continue``.

``header_bl…

`def _wants_100_continue(header_block: bytes) -> bool`

Return True if the header block declares`Expect: 100-continue`.

`header_block`is the raw bytes between the request line and the
blank-line terminator. Each CRLF-delimited line is checked so that an
`expect:`substring inside the request target or another header value
cannot trigger a false match; full header validation still happens in
`_fast_h1.parse_request` (/pounce/api/_fast_h1/#parse_request).

##### Parameters

Name
Type
Description

`header_block`
`bytes`

##### Returns

`bool`

`_classify_request`

1

`_RequestMeta (/pounce/api/sync_worker/#_RequestMeta)`

▼

Single-pass header extraction for the sync-worker hot path.

Replaces separate …

`def _classify_request(request: RequestReceived) -> _RequestMeta`

Single-pass header extraction for the sync-worker hot path.

Replaces separate calls to`_wants_close`, `_is_websocket_upgrade`,
and`get_header(accept-encoding)`.
Header names are already lowered by `_fast_h1.parse_request` (/pounce/api/_fast_h1/#parse_request).

##### Parameters

Name
Type
Description

`request`
`RequestReceived (/pounce/api/protocols/_base/#RequestReceived)`

##### Returns

`_RequestMeta (/pounce/api/sync_worker/#_RequestMeta)`

`_finalize_response_headers`

6

`tuple[list[tuple[bytes, …`

▼

Rewrite response headers and body for the sync-worker response paths.

Local he…

`def _finalize_response_headers(response_headers: Sequence[tuple[bytes, bytes]], body: bytes, compressor: Compressor | None, dictionary: CompressionDictionary | None, config: ServerConfig, *, apply_min_size: bool) -> tuple[list[tuple[bytes, bytes]], bytes]`

Rewrite response headers and body for the sync-worker response paths.

Local helper shared by the SyncApp fast path and the inline ASGI path.
Performs the single-pass content-encoding / content-length rewrite,
applies the negotiated`compressor`(suppressed when the app pre-set a
`content-encoding` header), re-appends `content-length`, and — when
`dcz` was negotiated — emits `used-dictionary`.

`apply_min_size` gates compression on `config.compression_min_size`
(the SyncApp fast path); the inline ASGI path passes`False`because the
bridge already governs sub-threshold bodies, so the historical sync-ASGI
behaviour compresses regardless of body size.

Returns (headers_list, body_out). To preserve byte-for-byte header order,
callers append request-scoped headers (`x-request-id`), the
dictionary-advertisement headers (RFC 9842), and the connection header
after this returns.

##### Parameters

Name
Type
Description

`response_headers`
`Sequence[tuple[bytes, bytes]]`

`body`
`bytes`

`compressor`
`Compressor (/pounce/api/_compression/#Compressor) | None`

`dictionary`
`CompressionDictionary (/pounce/api/_compression/#CompressionDictionary) | None`

`config`
`ServerConfig (/pounce/api/config/#ServerConfig)`

`apply_min_size`
`bool`

##### Returns

`tuple[list[tuple[bytes, bytes]], bytes]`
