If you build Camunda connectors long enough, you stop thinking about the happy path. You start thinking about rate limits, flaky networks, and APIs that return the same error for five different root causes. That’s where retry logic in a connector becomes part of the connector design.
The tricky part is that retries are not automatic. In Camunda, an outbound connector runs as a job in the connector runtime. You can configure retries (and retry backoff) on the BPMN service task in Camunda Modeler, but the connector still needs to tell the Camunda engine when a failure is retryable. If you throw the wrong exception type from the Camunda Connector SDK, the engine won’t retry. In the worst case, you’ll see retries get set to 0 and your activity level retry settings are ignored for that failure path. The message of this post is simple: know your errors and act accordingly.
I’ll use a small REST API example where the connector gets a rate limit response. Of course, Java technologies like WebFlux WebClient can be wrapped with client side retries, but for this article I want the Camunda engine to retry (including backoff) so the behavior is visible in Operate and consistent across connectors.
Why Retries Matter
Imagine that your Camunda connector calls an external REST API. Most of the time, it works. Sometimes, the API rate limits you and returns something like this:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json; charset=utf-8
Retry-After: 30
X-Request-Id: 049c9564-e6c7-7bc3-8a47-d955de067599
{
"id": "019c9f14-b575-722b-a0c2-f1ac5834faef",
"error": "Too Many Requests",
"message": "Rate limit exceeded.",
"timestamp": "2026-02-27T12:34:26Z"
}If you fail fast with a non retry-able error, you create incidents for something that would have resolved itself in 30 seconds.
That is the key difference between a good connector and a noisy connector. A good connector turns transient failures into retries and permanent failures into clear incidents in Camunda Operate.
Force The Camunda Engine To Retry
The Camunda engine can only apply the retry configuration when the connector tells it that a task is retry-able. Otherwise it can end up creating an incident even though you configured retries.
For connectors this makes sense: a low-code modeler can set max retries and backoff on the BPMN task, but they usually don’t know how the connector is implemented. They can define the frame, but the connector decides whether it’s applied. The connector runtime sees the HTTP status, response body, and Java exception types, so it is the only place that can reliably decide between “try again later” and “this will never work”. With job workers, the situation is often different. A technical team usually implements the worker, wires the variables, and owns the activity configuration, so retry behavior is part of the worker contract. That is why connector retries need an explicit error classification policy inside the connector code.
In the Camunda Connector SDK for Java, that signal is a ConnectorRetryException (created via ConnectorRetryExceptionBuilder). If you throw a generic exception or a plain ConnectorException, the connector runtime treats it as non retry-able and the task level retry settings are effectively ignored for that failure path.
As soon as you throw ConnectorRetryException, the retry configuration on the task becomes active. You do not need to encode “retry three times” into the exception. The task already knows how many retries and what backoff it should use.
Classify Errors And Exceptions
Start with a small classification table. Keep it intentionally boring. HTTP status codes are a good starting point, but also exceptions from underlying libraries:
Retryable
Rate limits (HTTP 429)
Some server errors (like temporary errors: HTTP 502, 503 or 504)
Timeouts (SocketTimeoutException)
Temporary network failures (UnknownHostException, ConnectException)
etc.
Non retryable
Authentication and Authorization (HTTP 401, HTTP 403)
Invalid input (HTTP 400)
Missing resources (HTTP 404)
Some server errors (like an unspecific HTTP 500)
Most other HTTP 4xx responses, unless you have a clear reason to retry
Code Examples
This example shows one Camunda outbound connector function that calls a REST API and throws either ConnectorException (non retryable) or ConnectorRetryException (retryable). These exception types come from the Camunda Connector SDK. The important part is the mapping, not the HTTP client details.
Non Retryable: 401 Unauthorized
If credentials are wrong, retries won’t fix it. Fail fast with a clear error message and keep the root cause.
public CustomerResponse fetchCustomer(String customerId) {
try {
return webClient
.get()
.uri("/customers/{id}", customerId)
.retrieve()
.bodyToMono(CustomerResponse.class)
.block();
} catch (WebClientResponseException.Unauthorized e) {
throw new ConnectorRetryExceptionBuilder()
.message("API request failed: not authorized (401)")
.errorCode("UNAUTHORIZED")
.cause(e)
.retries(0)
.build();
}
}Retryable: 429 Too Many Requests
If you get rate limited, throw ConnectorRetryException. This activates the retry configuration on the task.
public CustomerResponse fetchCustomer(String customerId) {
try {
return webClient
.get()
.uri("/customers/{id}", customerId)
.retrieve()
.bodyToMono(CustomerResponse.class)
.block();
} catch (WebClientResponseException.TooManyRequests e) {
throw new ConnectorRetryExceptionBuilder()
.message("API rate limited us (429). Please retry.")
.errorCode("RATE_LIMIT")
.build();
}
}Advanced: Retry-After And Backoff
Many APIs return a Retry-After header with status 429. If you have it, you can use it as a retry backoff candidate for the next attempt. You still rely on the task retry configuration for the overall retry behavior, but this gives the Camunda engine a better starting point for the next retry.
} catch (WebClientResponseException.TooManyRequests e) {
Duration retryAfter = parseRetryAfterHeader(e.getHeaders()); // example helper
ConnectorRetryExceptionBuilder builder = new ConnectorRetryExceptionBuilder()
.message("API rate limited us (429). Please retry.")
.errorCode("RATE_LIMIT");
if (retryAfter != null) {
builder.backoffDuration(retryAfter);
}
throw builder.build();
}Some Additional Notes
These are a few advanced topics that often come up once a connector runs under real load. They help you prevent new failure modes while you add retries.
Pick one retry layer. Mixing e.g. WebFlux retries and Camunda engine retries is usually a bad idea because you multiply calls and make backoff unpredictable.
Think about idempotence. Retrying a “send mail” connector can send the same mail twice if you retry the wrong failure. If the operation has side effects, you either need idempotency keys, deduplication, or a very strict retry policy.
Always keep the root cause. Whatever you throw should include the original exception as the cause. It will show up in Operate, and someone needs to understand what actually went wrong.
Key Takeaways
The Camunda engine retries only when you throw ConnectorRetryException.
If you want to create an incident right away, set retries to 0.
Model retries and backoff live on the BPMN service task, not in your code.
Classify exceptions as non retryable and retryable.
Honor meta information like Retry-After as a backoff hint when you get it.
Don’t mix client side retries with Camunda engine retries.
Retry only idempotent operations, or make them idempotent.
Always attach the root cause so Camunda Operate shows what happened.

