What is the Sidecar Pattern?
The Sidecar Pattern deploys a helper component alongside a primary application to provide supplementary functionality. The sidecar runs in the same process space (container, pod, or host) as the main application and shares its lifecycle. This pattern enables adding features like logging, monitoring, configuration, and networking without modifying the application code.
Sidecar Use Cases
- Logging & Monitoring: Collect and forward logs, metrics, and traces (Fluentd, Datadog agent)
- Proxy & Networking: Handle mTLS, load balancing, circuit breaking (Envoy, Linkerd proxy)
- Configuration: Watch for config changes and update the app (Consul agent, ConfigMap watcher)
- Security: Handle authentication, authorization, and secret management (Vault agent)
Sidecar Implementation
// Logging sidecar that collects and ships logs
class LoggingSidecar {
private logBuffer: LogEntry[] = [];
private flushInterval: NodeJS.Timeout;
constructor(
private readonly logSource: string, // File path or socket
private readonly logShipper: LogShipper,
private readonly batchSize: number = 100,
private readonly flushMs: number = 5000
) {
this.flushInterval = setInterval(() => this.flush(), flushMs);
}
async start(): Promise {
// Watch log source (file tail, Unix socket, HTTP endpoint)
const watcher = new LogWatcher(this.logSource);
watcher.on("line", (line: string) => {
try {
const entry = this.parseLine(line);
this.logBuffer.push(entry);
if (this.logBuffer.length >= this.batchSize) {
this.flush();
}
} catch (error) {
console.error("Failed to parse log line:", error);
}
});
await watcher.start();
}
private async flush(): Promise {
if (this.logBuffer.length === 0) return;
const batch = this.logBuffer.splice(0);
try {
await this.logShipper.ship(batch);
} catch (error) {
// Re-add to buffer on failure
this.logBuffer.unshift(...batch);
console.error("Failed to ship logs:", error);
}
}
private parseLine(line: string): LogEntry {
// Parse structured logs (JSON) or unstructured logs
try {
return JSON.parse(line);
} catch {
return {
timestamp: new Date().toISOString(),
level: "info",
message: line,
source: this.logSource,
};
}
}
stop(): void {
clearInterval(this.flushInterval);
this.flush();
}
}
Ambassador Pattern
The Ambassador Pattern is a specialized sidecar that acts as a proxy between the application and external services. It handles connection pooling, retries, circuit breaking, and protocol translation — allowing the application to make simple local calls while the ambassador handles the complexity of remote communication.
// Ambassador: Proxy to external Redis with connection pooling and retries
class RedisAmbassador {
private pool: RedisConnection[] = [];
private circuitState: "closed" | "open" | "half-open" = "closed";
private failureCount = 0;
private lastFailure: Date | null = null;
constructor(
private readonly redisUrl: string,
private readonly poolSize: number = 10,
private readonly failureThreshold: number = 5,
private readonly recoveryTimeout: number = 30000
) {}
async get(key: string): Promise {
if (this.circuitState === "open") {
if (Date.now() - this.lastFailure!.getTime() > this.recoveryTimeout) {
this.circuitState = "half-open";
} else {
throw new Error("Circuit breaker is open");
}
}
const connection = await this.acquireConnection();
try {
const result = await this.executeWithRetry(() => connection.get(key), 3);
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
} finally {
this.releaseConnection(connection);
}
}
private async executeWithRetry(
fn: () => Promise,
maxRetries: number
): Promise {
let lastError: Error | null = null;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 100));
}
}
throw lastError;
}
private onSuccess(): void {
this.failureCount = 0;
this.circuitState = "closed";
}
private onFailure(): void {
this.failureCount++;
this.lastFailure = new Date();
if (this.failureCount >= this.failureThreshold) {
this.circuitState = "open";
}
}
}
// Application uses the ambassador through a simple local interface
// The ambassador runs as a sidecar container accessible via localhost
const redis = new RedisAmbassador("redis://redis-cluster:6379");
const value = await redis.get("user:123");
Adapter Pattern (Sidecar Variant)
The Adapter Pattern sidecar normalizes or transforms the interface of the main application to match what external systems expect. Common uses include converting log formats, translating metrics to Prometheus format, or adapting health check endpoints.
// Adapter sidecar: Convert custom metrics to Prometheus format
class PrometheusAdapter {
private metrics: Map = new Map();
constructor(private readonly appMetricsUrl: string) {}
async start(): Promise {
// Poll the application's custom metrics endpoint
setInterval(async () => {
const response = await fetch(this.appMetricsUrl);
const appMetrics = await response.json();
this.convertMetrics(appMetrics);
}, 10000);
// Expose Prometheus-compatible /metrics endpoint
const server = express();
server.get("/metrics", (req, res) => {
res.set("Content-Type", "text/plain; charset=utf-8");
res.send(this.toPrometheusFormat());
});
server.listen(9090);
}
private convertMetrics(appMetrics: Record): void {
// Convert app-specific format to Prometheus format
if (appMetrics.requestCount) {
this.metrics.set("http_requests_total", {
type: "counter",
value: appMetrics.requestCount as number,
help: "Total HTTP requests",
});
}
if (appMetrics.avgResponseTime) {
this.metrics.set("http_response_seconds", {
type: "gauge",
value: (appMetrics.avgResponseTime as number) / 1000,
help: "Average HTTP response time in seconds",
});
}
}
private toPrometheusFormat(): string {
let output = "";
this.metrics.forEach((metric, name) => {
output += `# HELP ${name} ${metric.help}\n`;
output += `# TYPE ${name} ${metric.type}\n`;
output += `${name} ${metric.value}\n`;
});
return output;
}
}
Sidecar Variants Comparison
| Pattern | Direction | Purpose |
|---|---|---|
| Sidecar | Extends the app | Add cross-cutting concerns (logging, monitoring) |
| Ambassador | App to external | Proxy outbound calls with retries, circuit breaking |
| Adapter | External to app | Normalize app interfaces for external consumers |
Best Practices
- Lifecycle coupling: The sidecar must start with the app and stop with the app — use Kubernetes init containers and preStop hooks
- Resource limits: Set CPU and memory limits on sidecars to prevent them from starving the main application
- Health checks: Include the sidecar in the pod's readiness checks — the app is not ready until the sidecar is
- Keep sidecars generic: Sidecars should be reusable across services — avoid app-specific logic in them