A high-performance Go client for Camunda 7 external tasks, supporting both traditional REST polling and ultra-high-throughput WAL-based Change Data Capture (CDC) processing using Sequin.
This client supports two distinct execution patterns depending on your system load requirements and deployment complexity:
In the standard pattern, the client periodically sends /fetchAndLock REST requests to the Camunda Engine.
- Pros: Simple, zero database integration or CDC setups required. Works with any Camunda 7 database.
- Cons: High database & network polling overhead at rest. High concurrent completions on parallel multi-instances can trigger
OptimisticLockingExceptionsinside Camunda's API, causing 60-second lock timeouts under peak loads.
Instead of polling the REST API, the CDC architecture captures task insertion events directly from the PostgreSQL Write-Ahead Log (WAL) using Sequin stream consumer, and processes tasks with optimized REST-based locking. The worker has zero direct connection to the Camunda database.
graph TD
subgraph "Go Client (Worker App)"
Worker["SequinWorker (camunda.SequinWorker)"]
TC["TaskCompletion (internal/tasks)"]
TF["TaskFailure (internal/tasks)"]
Handlers["Registered TaskHandlers"]
end
subgraph "CDC Pipeline Layer"
Sequin["Sequin Service (:7376)"]
WAL["Logical Replication Slot (sequin_slot)"]
end
subgraph "Camunda Process Engine"
REST["Camunda REST API (:8080)"]
DB["PostgreSQL / Database (:7477)"]
end
%% WAL CDC path
DB -->|WAL Logs| WAL
WAL -->|Logical CDC Stream| Sequin
Sequin -->|1. HTTP Pull Events| Worker
%% Task execution path
Worker -->|2. POST /lock| REST
Worker -->|3. GET /variables| REST
Worker -->|4. Run Handler| Handlers
Handlers -->|5. Complete Task| TC
TC -->|6. POST /complete| REST
REST -->|7. Commit Transaction| DB
- Event Capture: When a new external task is created, a row is inserted into Camunda's
act_ru_ext_tasktable. PostgreSQL writes this transaction to the Write-Ahead Log (WAL). - Streaming via Sequin: Sequin captures this transaction through a logical replication slot (
sequin_slot) and publication (sequin_pub) and exposes it as an HTTP Pull queue. - HTTP Pull Delivery: The
SequinWorkerpulls messages from Sequin via/receive. Sequin's Visibility Timeout guarantees that this message is only delivered to one worker, eliminating the need for database-level concurrency locks. - REST Lock Activation: The worker locks the task via Camunda REST API (
POST /external-task/{id}/lock) to satisfy the engine completion requirements. - Variable Querying: Once locked, the worker queries process variables via Camunda REST API (
GET /process-instance/{id}/variables). - Execution: The registered handler is executed.
- Task Completion: The handler finishes execution and uses the REST API (
/external-task/{id}/complete) to commit the task completion back to the Camunda Engine. - Acknowledgement: The worker sends an HTTP ACK request to Sequin to remove the processed event from the queue. If a transient error occurs (e.g.
OptimisticLockingException), the worker sends a NACK to Sequin, triggering an instant retry.
To enable CDC replication slots and publication schemas safely without editing default docker configs, we use Atlas Go. The configuration is version-controlled in atlas.hcl.
Migrations are automatically applied during deployment using the arigaio/atlas:latest-alpine runner container, which waits for Camunda schema tables to initialize before configuring replication:
20260531100000_init_sequin.sql: Creates the Sequin user, replication slot, and publication.20260531100001_enable_replica_identity.sql: ConfiguresREPLICA IDENTITY FULLonact_ru_ext_taskto ensure the CDC payloads capture complete row updates.
- Target Filtering: Restrict Sequin sinks to
"public.act_ru_ext_task"to avoid performance bottlenecks caused by variable or history table modifications. - HTTP Keep-Alives: Both REST and CDC workers share a tuned
http.Transportpool (MaxIdleConns = 100,MaxIdleConnsPerHost = 100,IdleConnTimeout = 90s) to avoid macOS/Linux ephemeral port exhaustion (TIME_WAIT).
package main
import (
"context"
"log/slog"
"time"
"github.com/nativebpm/camunda"
)
func main() {
logger := slog.Default()
client, err := camunda.NewClient("http://localhost:8080", "classic-worker")
if err != nil {
logger.Error("Failed to init client", "error", err)
return
}
worker := camunda.NewWorker(client, logger)
worker.SetMaxTasks(20)
worker.SetPollInterval(100 * time.Millisecond)
worker.RegisterHandler("creditScoreChecker", func(ctx context.Context, client *camunda.Client, task camunda.ExternalTask, complete camunda.CompleteFunc, fail camunda.FailFunc) error {
// Business logic here
return complete().Variable("score", 750).Execute()
}, 60000, []string{"score"})
worker.Start(context.Background())
}package main
import (
"context"
"log/slog"
"github.com/nativebpm/camunda"
)
func main() {
logger := slog.Default()
// Initialize API client for completions, locks and variables fetching
client, err := camunda.NewClient("http://localhost:8080", "sequin-worker")
if err != nil {
logger.Error("Failed to init client", "error", err)
return
}
// Initialize Sequin worker with Sequin endpoint and consumer
sequinURL := "http://localhost:7376"
consumer := "camunda_tasks"
sequinWorker, err := camunda.NewSequinWorker(client, sequinURL, consumer, logger)
if err != nil {
logger.Error("Failed to create Sequin worker", "error", err)
return
}
sequinWorker.RegisterHandler("creditScoreChecker", func(ctx context.Context, client *camunda.Client, task camunda.ExternalTask, complete camunda.CompleteFunc, fail camunda.FailFunc) error {
// Business logic here
return complete().Variable("score", 750).Execute()
})
sequinWorker.Start(context.Background())
}Under benchmark testing deploying the loan-granting.bpmn workflow, we evaluated traditional REST polling and WAL CDC (using Sequin Pull consumer) under heavy scaling pressures:
- REST Polling: Aggressive polling at scale experiences lock wait states and transactional rollbacks, leading to 60-second lock timeouts under concurrent completion of parallel tasks.
- Sequin WAL CDC (Database-Free):
- 500 instances (2,047 tasks): Completed in 20.51s at 24.37 RPS / 99.79 TPS.
- 1000 instances (4,014 tasks): Completed in 71.52s at 13.98 RPS / 56.13 TPS (Latency metrics: p50=57s, p90=67.5s, p99=70.4s).
- 2000 instances (8,036 tasks): Completed in 22.41s at 89.26 RPS / 358.66 TPS (Latency metrics: p50=13.8s, p90=20.4s, p99=21.1s).
- 3000 instances (12,023 tasks): Completed in 46.34s at 64.74 RPS / 259.45 TPS (optimized concurrency control, G1GC tuning, structured decoding, and adaptive connection pooling).
- After Lock Optimization (Unlock before NACK): Introducing explicit
Unlockon optimistic transaction rollbacks (OptimisticLockingException) and smart NACK filtering on Lock failures slashed process p50 / p99 Latency from 30.4s / 44.6s to sub-second values (retries take milliseconds instead of waiting for Camunda's 30s lock timeout), while maintaining a 100% success rate with zero database deadlocks.
For full benchmark reports, refer to the detailed report.