[Feature Request] Support access to metric meter in multi-processed workers · Issue #1049 · temporalio/sdk-python · GitHub
Skip to content

[Feature Request] Support access to metric meter in multi-processed workers #1049

Open

Description

@gregbrowndev

Is your feature request related to a problem? Please describe.

Currently, the SDK doesn't extend custom metric support via the Runtime's MetricMeter to activities running within a multi-processed worker. This means users have to figure out how to support this themselves, which adds complexity to the project and may be infeasible for some.

Describe the solution you'd like

Ideally, I'd hope to see metrics wired up across the process pool out of the box. So following a similar approach to the existing heartbeat manager, metrics emitted by the child process would be sent via a multiprocessing queue to the parent worker process.

Possible high-level approach:

  • Provide temporalio.common.MetricMeter implementation alternate to _MetricMeter that is set in _Context automatically in process pool workers.
  • Instead of being backed by temporalio.bridge.metric.MetricMeter, they would push messages onto a queue consumed by the parent process, which would then rely those actions against the real temporalio.bridge.metric.MetricMeter.

This might be easier Python side, than trying to wire up the Rust code under the hood.

Worth pointing out there are on-going proposals to do this natively in OTEL: open-telemetry/opentelemetry-collector-contrib#4968.

I'd be happy to support this work, if you think the plan sounds reasonable.

Additional context

Background

The previous work to add custom metric support, #384, at the time didn't include support for multi-processed worker:

NOTE: This does not return a metric meter for sync activities that are non-threaded (i.e. multiprocess) because a meter cannot be shared across processes and it's a large effort to support metric shipping across processes for this rarely used activity type

Case to support this feature

The multi-processed worker provides a lot of value, at least on my project which uses Temporal for its typical system integration use cases but also for managing some reasonably heavy CPU bound workloads (e.g. processing PDFs, NLP, small batch jobs, etc.) all within one tool.

It might be the least frequently used worker type, but it provides several key benefits of its own:

  • It highlights a clear design principle to split up CPU bound workloads at the workflow level, i.e. fanning out, and then keep each activity single threaded/single processed to optimise performance, avoid under/over utilisation of the underlying CPU cores, and support better auto-scaling decisions. Its easier to understand when size and scheduling of tasks on the process pool is managed by the worker itself, rather than a free-for-all that could occur if async/threaded activities were to manage their own/shared processes or pools.
  • It provides out of the box support for heartbeating in activities running in the process pool, which users would need to wire up themselves if managing processes in the asyncio/threaded workers.
  • Python is a common data science language, so a process pool based worker would be the go to over async/threaded

Existing solution

Besides having to set up and configure a second OTEL metrics toolchain (MeterProvider, processors, exporters, and using different meter and metric classes), users would need to:

  • ensure the toolchain is initialised correctly to properly configure the process pool
  • be concerned with different default mp_context behaviours on different OSs, e.g. maybe they develop on MacOS which defaults to spawn, but deploy in a linux based image, which defaults to fork (the default will change to forkserver Python 3.14)
  • ensure with the global metric provider isn't accidentally inherited by the child process, which can cause deadlocks, or understand fork pre init hooks.

Once the process pool is configured, we'd then need to get metrics out of the process correctly, e.g.:

  • Tagging metrics with the process ID pid and using OTLPMetricExporter provides a simple solution, since each process would just push its own metrics to a collector. However, this would lead to high-cardinality, increased costs, and further complexity down the line.

    Note: the metric payloads scraped/pushed to the backend need to be uniquely labeled, otherwise you get out-of-order writes, conflicts, and are otherwise left with meaningless metric values interleaved from multiple ambiguous sources. (It can also cause you to exceed the platform's data points per minute (DPM), which in Grafana Clouds case DPM=1, i.e. scrape/emit every 60s.

  • The initial simplicity would then come at the cost of having to set up some external metric aggregation. E.g. within an OTEL collector's transformprocessor using aggregate_on_attributes to aggregate out the PID label. This also requires sensitive handling using interval, batch, metricstarttime, etc. to ensure you capture all the metrics within one interval.

Activity

  1. tconley1428 commented on Aug 25, 2025

    @tconley1428
    Contributor

    This does seem like a valuable feature for multi process workers, but that isn't a very large scenario at the moment and we have limited bandwidth. We'll put it up for triage, but it isn't very likely to be prioritized soon.

  2. gregbrowndev commented on Aug 26, 2025

    @gregbrowndev
    Author

    Thanks for considering the request @tconley1428 🙏🏻

    I understand the bandwidth constraints (I hope the Nexus work is going well!). Would it be helpful to receive contributions in this area? I'd quite like to drive forward observability related features for my own project's sake and to give back to the project.


    I did spend quite a bit of time over the weekend digging into the Rust SDK code to better understand whats happening under the hood.

    For me, most of the SDK's latency histogram metric are basically a bit useless and expensive, since we can't specify bucket overrides in OtelConfig like you can in the Prometheus config and all of my tasks are long running and even scheduled_to_start latencies go higher than the default settings (very much specific to my own use case, I know). So even if I solved this issue, I'd still be constrained by the bucket settings in the Rust code. Though, ideally, we'd be able to export exponential histogram formats.

    Additionally, the currently metric meter doesn't support other advanced features like capturing trace exemplars (trace-to-metrics), as it would mean adding that to the abstraction / dynamically loading OTEL's tracer within the metric meter some how...

    So I'm just wondering now whether the SDK would be better off with a Python-first OTEL integration built on top of the MetricBuffer to access the raw SDK datapoints and then have access to all the features, like views, resource, tracing-integration, etc. all configurable within the standard Python OTEL SDK.

    Is that something that is being considered? Otherwise, I'd be happy to be able to contribute towards these missing features if the [language] -> Rust approach is the intended solution going forward.

    Cheers

  3. Sushisource commented on Sep 3, 2025

    @Sushisource
    Member

    @gregbrowndev Thanks for all this great feedback.

    So, I agree that probably the best option here is to use the MetricBuffer as you suggest, so that you can directly do the more advanced things you'd like to do in Python using the exporter directly. It is, indeed, exactly what MetricBuffer is intended for.

    Something using it to export via OTel could I think be added to contrib here: https://github.com/temporalio/sdk-python/blob/main/temporalio/contrib/opentelemetry.py and would be a welcome contribution

  4. added 2 commits that reference this issue on Aug 18, 2026
    3a94445
    5ffb9a2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions