Activity retry event for UI state reset
When an Activity can retry, publish a RETRY event with activity.GetInfo(ctx).Attempt when attempt > 1. This lets the UI respond appropriately to the failure, typically by clearing accumulated deltas before the next attempt's deltas arrive. A retried attempt is a fresh publisher, so its output appears in the stream alongside output from the previous attempt.
Activity Dependency Injection pattern definition
The Activity Dependency Injection pattern separates the creation of external dependencies (database connections, API clients, configuration) from Activity business logic by injecting them at Worker startup. This approach keeps Workflow code deterministic, makes Activities testable in isolation, and ensures expensive resources are initialized once per Worker process rather than once per Activity execution.
Python Activity dependency injection pitfall: static method registration
In Python, registering class methods as static (registering BotService.send_message, the unbound method) instead of bot_service.send_message (a method on an instance) leaves the self parameter unbound, causing a missing argument error at runtime.
Activity struct definition pattern in Go
In Go, Activities are defined as methods on a struct that holds dependencies as fields. The struct is instantiated once at Worker startup with real implementations and then registered with the Worker. At execution time, a nil pointer of the Activity struct type (var a *Activities) provides compile-time method references without instantiating the struct, and the Temporal runtime resolves the actual registered instance at execution time.
Activity class definition pattern in Python
In Python, Activities are defined as methods on a @dataclass with @activity.defn decorators. Fields hold dependencies. The class is instantiated once at Worker startup with real implementations. The workflow.execute_activity_method references the class method directly and resolves to the registered instance on the Worker.
Activity interface and implementation pattern in Java
In Java, Activities use an @ActivityInterface with a separate implementation class. Dependencies are passed through the constructor to the implementation. Workflow.newActivityStub creates a typed proxy from the Activity interface, and the Temporal runtime routes calls to the registered implementation.
Activity factory function pattern in TypeScript
In TypeScript, a factory function closes over dependencies and returns an object of Activity functions. The proxyActivities<ReturnType<typeof createActivities>> infers the Activity types from the factory function's return type. Activities are always referenced by name at runtime.
Dependencies initialization in Go worker registration
In Go, at Worker startup, you create dependency instances and inject them into an Activity struct when registering: w.RegisterActivity(&payment.Activities{DBClient: payment.NewPostgresClient(...), EmailClient: payment.NewSMTPClient(...)}). Dependencies are initialized once when the Worker process starts, and all Activity executions on that Worker share the same instances.
Dependencies initialization in Python worker registration
In Python, at Worker startup, instantiate the activity class with dependencies and pass both the class methods and the dependency instances to the Worker: payment_activities = PaymentActivities(db_client=PostgresClient(...), email_client=SMTPClient(...)) followed by activities=[payment_activities.charge_customer, payment_activities.send_receipt].
Dependencies initialization in Java worker registration
In Java, at Worker startup, instantiate the implementation class with dependencies and register it: worker.registerActivitiesImplementations(new PaymentActivitiesImpl(new PostgresClient(...), new SMTPClient(...))).
Dependencies initialization in TypeScript worker registration
In TypeScript, initialize dependencies at Worker startup and pass them to the factory function through the activities property: activities: createActivities(db, emailClient).
Activity dependency injection pitfall: non-thread-safe dependencies
Using non-thread-safe dependencies in Worker-level injection causes race conditions. A single mutable object shared across concurrent Activity executions is unsafe. Use connection pools and ensure all injected objects are safe for concurrent use.
Keep dependencies thread-safe in Activity injection
Multiple Activity executions run concurrently on the same Worker. Injected dependencies must be thread-safe. Use connection pools rather than single connections, and avoid mutable shared state. All injected objects must be safe for concurrent use.
Define dependencies as interfaces for Activity injection
In Go, Python, and Java, using interfaces (or protocols in Python) for dependencies makes it possible to swap implementations for testing or different environments. This enables mock implementations in tests without modifying production code.
Initialize dependencies before Worker startup
Create and validate all dependency connections before calling worker.Run() or its equivalent. This ensures that the Worker does not start accepting tasks until all dependencies are ready.
Group related Activities on single struct or class
Activities that share the same dependencies belong together on a single struct or class. If two groups of Activities have different dependencies, use separate structs or classes for each group.
Activity dependency injection pitfall: constructing per-execution
Creating a new database connection or API client per Activity execution leads to resource exhaustion and increased latency. This is a common pitfall to avoid.
TypeScript Activity dependency injection pitfall: unbound methods
In TypeScript, when using a class instead of a factory function, class methods must be defined as arrow functions or explicitly bound in the constructor. Otherwise, 'this' is undefined when Temporal invokes the Activity.
Benefits of Activity dependency injection: resource efficiency
Resources like connection pools are initialized once and shared across all Activity executions, reducing overhead compared to creating new resources per execution.
Benefits of Activity dependency injection: testability
Substituting mock implementations in tests requires no changes to Activity or Workflow code when dependencies are injected. This enables Activities to be tested in isolation with test doubles.
Benefits of Activity dependency injection: environment switching
Switching between environments (development, staging, production) involves changing only the Worker configuration. Different environments can use different dependency implementations without modifying Activity or Workflow code.
Trade-off of Activity dependency injection: shared instances
All Activity executions on a given Worker share the same dependency instances. If an Activity requires per-execution isolation (for example, a database transaction scoped to a single Activity), you need to manage that isolation within the Activity method itself.
When not to use Activity dependency injection
This pattern is not necessary for Activities that are pure functions with no external dependencies, or for Activities that only use Temporal-provided context like heartbeating and logging.
When to use Activity dependency injection
This pattern is a good fit when Activities access external services such as databases, message queues, or third-party APIs, when you want to initialize expensive resources once per Worker process, when you need to test Activity logic without connecting to real services, or when you operate in multiple environments requiring different dependency configurations.
Circuit breaker as a stateful dependency
A circuit breaker is a stateful dependency that tracks recent failures for a downstream service. It should be injected at Worker level because its state (tracking failures and trips) must be shared across all Activity executions. Constructing a new breaker inside each Activity method would reset the counters on every call, so the breaker would never trip. A single breaker instance injected alongside the client it guards ensures all executions feed the same failure window.
Fast/Slow Retries best practice: log phase transition
Log the transition from Phase 1 to Phase 2 with enough context — request identifier, attempt count, timestamp — because this transition is a meaningful signal that the downstream system may have a sustained problem.
Fast/Slow Retries best practice: leave Phase 2 MaximumAttempts unset
Omit MaximumAttempts (or set it to 0) in Phase 2 to enable unlimited retries. The Temporal Service manages the wait between attempts via InitialInterval; the Workflow blocks until the Activity eventually succeeds.
Fast/Slow Retries best practice: combine with metrics
Add a metric counter inside the Activity in Phase 2 to surface slow-phase attempts to on-call teams, enabling proactive alerting for sustained failures.
Idempotency keys protect against duplicate side effects
An idempotency key—a stable identifier derived from the Workflow and Activity IDs—lets the downstream system detect and discard duplicates if a retry is needed in the future. When you have idempotency keys, there is little need to cap retries at 1, as the downstream system can safely handle retried calls without producing duplicate side effects.
Best practices for Fixed Count of Retries pattern
Match the cap to the cost model: if the API charges per call, set maximum_attempts to the maximum number of calls you are willing to pay for per Workflow execution. Combine with StartToCloseTimeout to prevent a slow response from consuming the entire retry budget on a single hanging call. Catch ActivityError in the Workflow and handle the exhausted-retries case explicitly by logging, alerting, compensating, or escalating rather than letting it fail the Workflow silently. Use idempotency keys so retries do not produce duplicate downstream effects. Prefer non-retryable errors for structural failures—if the failure is not transient such as invalid input, mark it as non-retryable rather than relying solely on maximum_attempts.
Set both timeouts for clarity in Fixed Wall-Time Retries
Use ScheduleToCloseTimeout as the total SLA and StartToCloseTimeout as a per-attempt safety valve. Omitting StartToCloseTimeout means a single slow response can consume the entire budget. Both timeouts should be set for clarity and to prevent unintended behavior.
Cap MaximumInterval well below the SLA
If MaximumInterval is set too high relative to the SLA, only a small number of retries are possible. For example, if MaximumInterval is 2 hours and the SLA is 24 hours, only 12 retries are possible. Tune the interval so the backoff plateaus at a value that allows meaningful retries within the budget.
Heartbeat best practice: set heartbeat timeout
Configure the heartbeat timeout to 2-3x the expected heartbeat interval.
Heartbeat best practice: heartbeat at regular intervals
Balance between responsiveness (every 10-30 seconds) and overhead when deciding heartbeat frequency.
Heartbeat best practice: checkpoint strategically
Save progress at meaningful boundaries (records, pages, chunks).
Heartbeat best practice: keep details small
Store minimal state (IDs, offsets, counts) in heartbeat details, not full objects.
Heartbeat best practice: handle idempotency
Ensure reprocessing the last checkpoint is safe, as Activities may reprocess the final checkpoint after resuming from heartbeat details.
Heartbeat best practice: check cancellation
Heartbeat regularly to detect cancellation quickly, as cancellation is only delivered on the next heartbeat.
Heartbeat best practice: clean up on cancel
Handle cancellation errors appropriately: catch ActivityCompletionException (Java), CancelledFailure (TypeScript), asyncio.CancelledError (Python), or check ctx.Done() (Go).
Heartbeat best practice: log progress
Log heartbeat details for debugging and monitoring.
Heartbeat best practice: test resumption
Verify Activities resume correctly after simulated failures.
Heartbeat best practice: avoid heartbeat spam
Do not heartbeat on every iteration of tight loops.
Resumable Activity best practice: bounded retries before parking
Allow a few automatic retries to recover from transient failures before parking. Parking immediately on the first failure forces operators to intervene for problems that would have resolved on their own.
Tuning Lambda worker for long-running activities
If your Worker handles long-running Activities, increase GracefulShutdownTimeout, ShutdownDeadlineBuffer, and the Lambda invocation deadline (--timeout) together.
Idempotency in Activities via event_id deduplication
Activities should be designed to be idempotent by using an event_id as a deduplication key. On retry with the same event_id, the Activity should detect and return the previous result without creating a duplicate record. Example: INSERT ... ON CONFLICT (event_id) DO NOTHING in SQL.
Python asyncio.to_thread for blocking I/O in activities
Use asyncio.to_thread() to avoid blocking the event loop when performing blocking file operations. Example: await asyncio.to_thread(Path(local_path).write_bytes, response.content) for writing files, and await asyncio.to_thread(Path(local_path).read_bytes) for reading files. This allows the Worker to handle other tasks while I/O completes.
Local Activities re-execute on Workflow Task timeout
If you run Local Activities, a Workflow Task timeout makes them run again from the start on the retried task. Their results are not written to Event History between Workflow Task heartbeats, so there is nothing to resume from. If they are not idempotent, you get duplicate side effects.
Activity heartbeat payload size impacts service memory
The last heartbeat details payload is held in memory for the life of the Activity attempt. Large payloads on high-throughput Activity Workers contribute to memory pressure on the Temporal Service. Store only the minimum progress state needed to resume on retry.