What (actions) cannot run in parallel

Is there a list of actions that for sure cannot be run in parallel? I just did some test and the first 2 actions I tried (“write to system log” and “send event”) turned out not being able to run in parallel :slight_smile:

Additional question: does Home Assistant ever use threads for parallel execution, or only uses context-switching?

Here's how I tested (script)
sequence:
  - parallel:
      - sequence:
          - variables:
              my_var: v1
          - repeat:
              count: 20
              sequence:
                - action: system_log.write
                  data:
                    level: warning
                    message: "{{ my_var }}: {{ repeat.index }}"
      - sequence:
          - variables:
              my_var: v2
          - repeat:
              count: 20
              sequence:
                - action: system_log.write
                  data:
                    level: warning
                    message: "{{ my_var }}: {{ repeat.index }}"
      - sequence:
          - variables:
              my_var: v3
          - repeat:
              count: 20
              sequence:
                - action: system_log.write
                  data:
                    level: warning
                    message: "{{ my_var }}: {{ repeat.index }}"
      - sequence:
          - variables:
              my_var: v4
          - repeat:
              count: 20
              sequence:
                - action: system_log.write
                  metadata: {}
                  data:
                    level: warning
                    message: "{{ my_var }}: {{ repeat.index }}"
alias: test1
description: ""
mode: single

Log output from that script (shortened):

v1: 1
v1: 2
(...)
v1: 20
(...)
v2: 1
(...)
v2: 20
(...)
v3: 1
(...)
v3: 20
(...)
v4: 1
(...)
v4: 20

When I replace

- action: system_log.write
  data:
    level: warning
    message: "{{ my_var }}: {{ repeat.index }}"

with:

- event: my_event
  event_data:
    my_var: "{{ my_var }}"
    index: "{{ repeat.index }}"

and then listen to that event in developer tools, I see that result is the same (sequences run one-by-one, not in parallel). However, when I add a delay after each write to log/send event:

- delay:
    milliseconds: 1

then execution goes like this:

v1: 1
v2: 1
v3: 1
v4: 1
v1: 2
v2: 2
v3: 2
v4: 2
(...)
v1: 20
v2: 20
v3: 20
v4: 20

Still not truly parallel but execution switches context. Also, I tried running each single sequence by 4 automations, that triggered at the same time. The results were the same as with single script+parallel.

Maybe the time to fork a process is so long that the count to 20 in the original process is handled before the second process is able to start.

I increased repeat count to 1k and I made each sequence wait for a boolean switch. I also added timestamps to the variables v1/v2/v3/v4 (to see when they were created).

Code
sequence:
  - parallel:
      - sequence:
          - variables:
              my_var: "v1_{{ now().timestamp()|string }} "
          - wait_template: "{{ is_state('input_boolean.test','on') }}"
            continue_on_timeout: true
          - repeat:
              count: 1000
              sequence:
                - action: system_log.write
                  metadata: {}
                  data:
                    level: warning
                    message: >-
                      {{ my_var }}: {{ repeat.index }} | {{
                      now().timestamp()|string }}
      - sequence:
          - variables:
              my_var: "v2_{{ now().timestamp()|string }} "
          - wait_template: "{{ is_state('input_boolean.test','on') }}"
            continue_on_timeout: true
          - repeat:
              count: 1000
              sequence:
                - action: system_log.write
                  metadata: {}
                  data:
                    level: warning
                    message: >-
                      {{ my_var }}: {{ repeat.index }} | {{
                      now().timestamp()|string }}
      - sequence:
          - variables:
              my_var: "v3_{{ now().timestamp()|string }} "
          - wait_template: "{{ is_state('input_boolean.test','on') }}"
            continue_on_timeout: true
          - repeat:
              count: 1000
              sequence:
                - action: system_log.write
                  metadata: {}
                  data:
                    level: warning
                    message: >-
                      {{ my_var }}: {{ repeat.index }} | {{
                      now().timestamp()|string }}
      - sequence:
          - variables:
              my_var: "v4_{{ now().timestamp()|string }} "
          - wait_template: "{{ is_state('input_boolean.test','on') }}"
            continue_on_timeout: true
          - repeat:
              count: 1000
              sequence:
                - action: system_log.write
                  metadata: {}
                  data:
                    level: warning
                    message: >-
                      {{ my_var }}: {{ repeat.index }} | {{
                      now().timestamp()|string }}
alias: test2
description: ""
fields: {}
mode: single

Results are in agreement with previous post: first, the four variables are created in order v1/v2/v3/v4:

v1_1740153975.02685
v2_1740153975.028302
v3_1740153975.030125
v4_1740153975.030884

Then the script waits for me to turn on that switch, and again, sequences of 1k repetitions are executed one-by-one:

v1_1740153975.02685: 1 		| 1740154019.321881
(...)
v1_1740153975.02685: 1000 	| 1740154019.978552
v2_1740153975.028302: 1 	| 1740154019.97924
(...)
v2_1740153975.028302: 1000 	| 1740154020.631791
v3_1740153975.030125: 1 	| 1740154020.632451
(...)
v3_1740153975.030125: 1000 	| 1740154021.316731
v4_1740153975.030884: 1 	| 1740154021.317548
(...)
v4_1740153975.030884: 1000 	| 1740154021.982135

You’re just comparing something that is added to the event loop vs something that just fires directly to the event bus. Events fire directly from the event bus, they don’t need to be added to a queue, where as service calls are added to the queue.

No, I compare an action to create event with an action to write to system log. I found that for both actions, concurrent execution doesn’t work but context switching is possible by injecting wait/delay/etc.

I wonder if this is a general rule in Home Assistant? I don’t say it’s wrong, actually it’s probably necessary. I’m trying to understand what actions are “safe” and what should I avoid in certain situations (e.g. trigger-based templates, where - as I now believe - you should avoid waiting/delaying, as it might cause them to miss an event).

I’m telling you what you’re comparing from a code perspective.

Events in ha (The event action) use hass.bus.fire which directly calls call_soon_threadsafe

where service call actions are called using asyncio.run_coroutine_threadsafe

here’s an explanation

If this is over your head, then all you need to know is:

Services will always behave like your test with system_log.write


Nothing in HA is parallel btw, it’s an event loop so something is always fired first from the main loop. Child threads are created & destroyed when the main loop needs (or doesn’t need) help.

What you’re seeing in your test is the functions getting added to the event loop in an order they will fire in.

EDIT: They both call from the event loop, sorry. I should be more explicit with my response.


The rule in HA is: First in first out of the event loop. It’s entirely based on how long it takes to get the coroutine or function into the event loop. It will likely be completely random and based on how fast your system is.

2 Likes

Thanks, I started to come to this conclusion too. The name “parallel” confused me and initially I expected it to work like e.g. in C#. Also what I wrote about trigger-based templates seems true. E.g. if you have an “action section” within trigger-based template:

action:
  - action: system_log.write
    data:
      level: info
      message: "{{trigger.event.data}}"
  - delay:
      milliseconds: 1

and you quickly send an event to trigger that template, you’ll see lots of warnings in log (“trigger already running”). But if you remove the delay, then it’s fine. I believe it’s because there can only be 1 instance of given trigger template running, and “delay” moves the completion of current routine further in the queue.

The action section for trigger based template entities is a script with mode single set.

1 Like