Deployment
Running rustcdc in production: process model, supervision, resource sizing and shutdown.
On this page
This guide documents local container deployment, runtime configuration, and production deployment baselines for rustcdc.
Deployment Models
rustcdc can be deployed as:
- embedded runtime in an application binary
- example container runtime for local validation
- production service managed by your platform scheduler
Reference Container Deployment
The repository includes a Docker-based reference path for the PostgreSQL example runtime.
Build Image
docker build -t rustcdc:postgres-example .Start Stack
docker compose up --build
The compose topology starts:
- PostgreSQL with logical replication enabled
- rustcdc example runtime configured with
CDC_RS_*variables
Runtime Configuration Variables
Common environment variables for the PostgreSQL example:
CDC_RS_HOSTCDC_RS_PORTCDC_RS_USERCDC_RS_PASSWORDCDC_RS_DBCDC_RS_SLOTCDC_RS_PUBLICATIONCDC_RS_SNAPSHOT_TABLESCDC_RS_CHECKPOINT_DIRCDC_RS_MAX_BUFFER_SIZECDC_RS_POLL_WAIT_MSCDC_RS_CONN_TIMEOUT_SECS
Persistent Storage Requirements
At minimum, production deployments must persist:
- checkpoint storage
- runtime logs
- optional schema history storage (backend-dependent)
If checkpoint storage is ephemeral, restart correctness is not guaranteed.
Production Baseline
Recommended baseline controls:
- run with explicit resource limits (CPU/memory)
- persist checkpoints on durable storage
- configure source credentials via managed secret systems
- expose runtime health and metrics endpoints through your control plane
- configure restart policy with backoff
- validate replay behavior before production cutover
Backpressure and Slow-Consumer Behavior
rustcdc uses cooperative flow control: the internal event buffer grows until it reaches max_buffer_size, at which point the runtime returns an error rather than accepting more work.
How it works
poll_event_batch()returns events from the internal buffer.commit_ack(batch.ack_mode())signals that the consumer has durably processed the acknowledged batch prefix. The runtime advances the checkpoint and frees buffer capacity.- If the consumer calls
poll_event_batch()repeatedly without callingcommit_ack(), the buffer fills tomax_buffer_sizeand the next call returnsError::Backpressure, whoseErrorKindisErrorKind::Backpressure.
Backpressure is not a failure — handle it accordingly
use rustcdc::{CdcRuntime, ErrorKind};
# async fn example(runtime: &mut CdcRuntime) -> rustcdc::Result<()> {
match runtime.poll_event_batch().await {
Ok(batch) => {
// …process, then acknowledge…
runtime.commit_ack(batch.ack_mode()).await?;
}
Err(error) if error.kind() == ErrorKind::Backpressure => {
// Normal flow control. Acknowledge the outstanding batch, then retry the
// same call — it will succeed. Do NOT treat this as fatal.
}
Err(error) if error.is_recoverable() => { /* transient source issue: retry with backoff */ }
Err(error) => return Err(error), // genuinely terminal
}
# Ok(())
# }
Match on
ErrorKind, not on message text. Backpressure has its own kind,ErrorKind::Backpressure, precisely so it is not confused withTerminal— "a permanent problem that retrying will not resolve". Treating routine buffer pressure as terminal shuts a healthy pipeline down. Its own kind means the two are distinguishable.
Limiting batch size in bytes
RuntimeOptions::max_event_bytes bounds the serialized size of a delivered batch, in
addition to the max_buffer_size event count. Set it when the downstream has a hard
message-size limit (Kafka max.message.bytes, SQS 256 KB, and so on).
A single event larger than the whole budget is still delivered on its own — refusing it would stall the pipeline permanently on one oversized row with no way to make progress.
Tuning guidance
| Scenario | Recommendation |
|---|---|
| Low-latency sink (in-memory, fast local writes) | Default max_buffer_size = 10_000 is appropriate. |
| Slow remote sink (network I/O, batching) | Increase max_buffer_size to absorb burst traffic; ensure commit_ack() is called promptly after each durable write. |
| Very slow consumer or bursty source | Combine a large max_buffer_size with an external queue (Kafka, SQS) between the runtime and the final sink. |
| Memory-constrained deployment | Reduce max_buffer_size and call commit_ack() as frequently as possible to keep the buffer shallow. |
⚠️ Important: Never call
poll_event_batch()in a tight loop without also callingcommit_ack(). The runtime cannot advance the checkpoint or reclaim buffer memory until acknowledgement is received. Monitorbuffer_depthviaadmin_snapshot()to detect a stalled consumer early.
Rollout Checklist
- validate source connector permissions in target environment
- verify snapshot and streaming behavior in staging
- verify checkpoint advancement under steady load
- execute restart and failover tests
- confirm downstream sink idempotency policy
Non-PostgreSQL Sources
For MySQL and SQL Server deployments, use the same runtime pattern with source-specific config and operational prerequisites from Library options and Embedded pipelines.
SQL Server: Supported Versions
| SQL Server Version | Status |
|---|---|
| 2025 | CI-tested (sqlserver_version_matrix): connect with CDC enabled, on 2025-CU10-ubuntu-24.04 |
| 2022 | CI-tested: every SQL Server suite, on 2022-CU27-ubuntu-22.04 |
| 2019 | CI-tested (sqlserver_version_matrix): connect with CDC enabled, on 2019-CU32-ubuntu-20.04 |
| 2016, 2017 | Accepted at startup (major version ≥ 13); not CI-tested |
| 2014 and earlier | Rejected at startup with Error::SourceError |
The image tags are pinned in crates/rustcdc/tests/sqlserver_testkit.rs.
The prerequisite probe in crates/rustcdc/src/source/sqlserver/query.rs checks for:
is_cdc_enabledon the target database.db_owner,sysadmin, ordb_ddladminrole membership.- Server major version ≥ 13 (SQL Server 2016). Versions below this are rejected at startup with an
Error::SourceError.
Related Documentation
HTTP Health and Admin Endpoint
rustcdc is an embeddable library and does not start an HTTP server. CdcRuntime exposes
admin_snapshot_json() which returns a RuntimeAdminSnapshot payload as JSON.
Wire it to any HTTP server of your choice.
Minimal axum example (add axum = "0.8", tokio and serde_json to your Cargo.toml):
// `ignore`: axum is not a dependency of this crate, so this block cannot be compiled here.
use std::sync::Arc;
use axum::{extract::State, response::IntoResponse, routing::get, Router};
use rustcdc::{CdcRuntime, RuntimeConfig, RuntimeSourceConfig};
use rustcdc::checkpoint::InMemoryCheckpoint;
use rustcdc::schema_history::InMemorySchemaHistory;
type SharedRuntime = Arc<tokio::sync::Mutex<CdcRuntime>>;
async fn health(State(rt): State<SharedRuntime>) -> impl IntoResponse {
let json = match rt.lock().await.admin_snapshot_json() {
Ok(payload) => payload,
Err(error) => {
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
[("content-type", "application/json")],
// `json!` rather than `format!`: an error message containing a quote
// or a newline would make hand-built JSON unparseable, and the one
// time this branch runs is the one time you need to read it.
serde_json::json!({ "error": error.to_string() }).to_string(),
);
}
};
(axum::http::StatusCode::OK, [("content-type", "application/json")], json)
}
#[tokio::main]
async fn main() {
let checkpoint = InMemoryCheckpoint::default();
let schema_history = InMemorySchemaHistory::default();
let config = RuntimeConfig::new(RuntimeSourceConfig::Disabled, checkpoint, schema_history);
let runtime = CdcRuntime::new(config).expect("runtime config should be valid");
let state: SharedRuntime = Arc::new(tokio::sync::Mutex::new(runtime));
let app = Router::new()
.route("/health", get(health))
.with_state(state.clone());
// Spawn the HTTP server alongside the CDC runtime.
tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
axum::serve(listener, app).await.unwrap();
});
// Run the CDC runtime loop in the foreground.
{
state.lock().await.start().await.unwrap();
loop {
let batch = {
let mut runtime = state.lock().await;
runtime.poll_event_batch().await.unwrap()
};
state.lock().await.commit_ack(batch.ack_mode()).await.unwrap();
}
}
}
The /health endpoint returns a JSON object such as:
{
"source_type": "postgres",
"state": "running",
"readiness": true,
"liveness": true,
"capabilities": {
"snapshot": true,
"snapshot_checkpoint_resume": true,
"handoff": true,
"ddl_capture": true,
"heartbeat": true,
"tls": true,
"schema_introspection": true
},
"buffer_depth": 0,
"in_flight_events": 0,
"snapshot_active": false,
"incremental_snapshot": {
"snapshot_id": "incremental-1716399000000",
"paused": false,
"tables": [
{
"table": "public.orders",
"pk_cursor": ["918433"],
"is_complete": false,
"chunks_emitted": 184,
"rows_emitted": 918433
}
]
},
"stream_active": true,
"handoff_complete": true,
"total_events_polled": 42817,
"total_events_committed": 42817,
"total_events_deduplicated": 0,
"started_at_ms": 1716400000000,
"last_poll_at_ms": 1716400000421,
"last_commit_at_ms": 1716400000440,
"checkpoint_age_ms": 12,
"replication_lag_ms": 4
}
Liveness probes should check for a 200 response and "liveness": true.
Readiness probes should additionally assert "readiness": true and may enforce
"state": "running" for stricter rollout policies.