Skip to content

Sync-over-Async

Guide: turn an asynchronous Kafka backend into a synchronous REST request/response across pods, with a Redis return route.

At a glance

  • What — a caller makes a normal synchronous REST call; behind it the request travels over Kafka to an asynchronous backend (possibly on another pod) and the response is routed back through Redis to the exact pod holding the open HTTP connection.
  • How — a correlation-id is the return-path key. The originating pod registers a return route in Redis and blocks; the responder stores the response in Redis and wakes the originator via Pub/Sub.
  • Advanced & opt-in — a distributed pattern for specific use cases; off by default (it eagerly connects to Redis). Cf. the service mesh — this is a deliberately lighter, Redis-based return path.
  • For developers exposing a cloud-native synchronous facade over an event-driven backend.

In a cloud-native deployment the REST caller and the backend that answers may be different pods, and the backend is reached asynchronously over Kafka. Sync-over-async bridges that gap: the HTTP thread waits while the request fans out over Kafka and the answer is routed back — without coupling the two pods beyond a shared Redis and topic pair. It builds on the Minimalist Kafka for the Kafka legs.

Opt-in. The return-route coordinator eagerly connects to Redis, so it starts only when sync.over.async.enabled=true. Leave it off unless you are using the pattern.

The pattern

sequenceDiagram
    participant Caller as REST caller
    participant Pod1 as service (POD-1)
    participant Redis
    participant Kafka
    participant Pod2 as backend service (POD-2)

    Caller->>Pod1: HTTP request (sync)
    Pod1->>Redis: register return route (key = correlation-id)
    Pod1->>Kafka: publish request
    note over Pod1: blocked HTTP thread waits<br>(suspended virtual thread)
    Kafka->>Pod2: consume request (async)
    Pod2->>Kafka: publish response
    Kafka->>Pod1: reply consumer receives response
    Pod1->>Redis: store response + wake via Pub/Sub
    Redis-->>Pod1: wake-up (Pub/Sub)
    Pod1-->>Caller: HTTP response (or 408 on timeout)

The correlation-id threads the whole round trip. Redis holds two short-lived keys per request: the route (which pod's channel to wake) and the response payload.

Enabling and configuring

sync.over.async.enabled=true
redis.host=${REDIS_HOST:127.0.0.1}
redis.port=${REDIS_PORT:6379}
redis.password=${REDIS_PASSWORD:}     # blank = no auth; keep secrets in the environment

On startup the extension builds a Redis client and the return-route coordinator, keyed by this pod's origin id, from discrete redis.* connection parameters and sync.* engine tunables. See the Configuration Reference for the full list; the essentials:

Key Default Description
sync.over.async.enabled false Master switch; true starts the return-route coordinator.
redis.host / redis.port 127.0.0.1 / 6379 Redis connection.
redis.password — (blank) Auth password; source from the environment.
redis.ssl false Use TLS (rediss://).
redis.database 0 Logical database index.
redis.timeout.ms 5000 Default command timeout.
sync.return.channel.prefix svc-return Prefix for the per-pod Pub/Sub return channel.
sync.route.ttl.seconds 90 TTL for the return-route key (cover the REST timeout + buffer).
sync.response.ttl.seconds 30 TTL for the response key (short rendezvous window).
sync.max.pending.requests 10000 Per-pod ceiling on in-flight synchronous requests (backpressure).

Reliability cornerstones

The design is correct independent of Pub/Sub timing:

  • Redis is the source of truth. The responder writes the response to Redis (SETEX) before sending the Pub/Sub wake-up, so a signal can never arrive before the data.
  • Final read before timeout. If the wake-up is missed, the waiting pod does one last Redis read before giving up — so a dropped notification still resolves the request rather than failing it.
  • Exactly-once completion. The pending-request registry completes each waiting future once, whichever of the wake-up path and the timeout path wins; duplicates and orphans are no-ops.
  • Bounded growth. sync.max.pending.requests caps in-flight requests to protect a pod under load.
  • Timeout → 408. A request with no answer in its budget returns HTTP 408, and its Redis keys are cleaned up (TTLs are the safety net for crashes).

When to use it

Reach for sync-over-async when you need a synchronous REST facade over an asynchronous, cross-pod backend and want a lightweight Redis return path rather than the full Kafka service mesh with presence discovery. Like the mesh, it is an advanced opt-in: if your application does not need cross-pod synchronous request/response, design it cloud-native and skip this. The Kafka legs use the Minimalist Kafka; the synchronous facade itself is an ordinary composable function behind rest.yaml.

See also