Capabilities
Everything in one binary
⇄
Reverse Proxy
Forwards HTTP traffic to a single upstream host and port. Preserves inbound X-Forwarded-For headers and sets X-Forwarded-* on outgoing requests.
🔍
Inspector Mode
Starts the target as a child process and auto-detects its listen port. Forwards signals and exits with the child's exit code.
📋
Structured Logging
Single-line minified JSON per request. Captures bodies, parses JSON/form/XML, computes structural shape hashes, logs headers and cookies.
☁
CloudWatch Delivery
Async bounded queue with batching, 2-second flush interval, and automatic retry on ThrottlingException. Separate log groups for traffic and health.
📊
EMF Metrics
Embedded Metric Format emitted to stdout and CloudWatch. Traffic and health metric namespaces with per-endpoint dimensions. No PutMetricData needed.
♻
Upstream Retry
Configurable retry policy with exponential backoff, per-method and per-status-code eligibility, and request-body replay for buffered bodies.
Normal Mode
Proxy a running app
Use normal mode when your application is already listening. cwproxy
connects to APP_HOST:APP_PORT and begins accepting traffic on PROXY_PORT.
It does not manage the upstream process lifecycle.
export APP_HOST=127.0.0.1
export APP_PORT=8080
export PROXY_PORT=8081
export APP_NAME=my-app
./cwproxy
Typical deployments: run beside a systemd service, as a sidecar container
in the same Pod, or in host-network mode where the upstream is already running.
Inspector Mode
Start and watch the target
Pass the target command after the binary name. cwproxy starts the
process, then polls its full process subtree every 100 ms for
open TCP LISTEN sockets. The lowest port found is used unless
APP_PORT is set explicitly.
# auto-detect port
./cwproxy node ./server.js
# explicit port override
APP_PORT=3000 ./cwproxy python app.py
- Child inherits the full
cwproxy environment and stdio.
SIGTERM and SIGINT are forwarded to the child process.
cwproxy exits with the child's exit code.
- Wrapper processes and short-lived launchers are supported — the port scanner walks the whole process tree.
Traffic Logging
One JSON line per request, everything included
Every proxied request produces a minified single-line JSON event on stdout.
The same event is delivered to CloudWatch Logs when AWS credentials are available.
Health probe paths are suppressed from traffic logs and emitted separately.
{
"_q":"my-app POST /orders 201 14.287ms", ← CloudWatch Logs Insights summary line (TRAFFIC only)
"_t":"TRAFFIC", ← category: TRAFFIC or HEALTH
"app_name":"my-app",
"global_hash":"a3f9c1", ← 6-char shape hash of query+req body+resp body
"delay":14.287,
"request": {
"method":"POST", "path":"/orders", "status": null,
"queries": {"source":"web"}, "queries_hash":"7b2e4a",
"headers": {"Content-Type":"application/json"},
"cookies": {},
"body": {"item":"widget","qty":3}, ← parsed JSON object
"body_raw":"{\"item\":\"widget\",\"qty\":3}", ← raw bytes always present alongside parsed
"body_hash":"c91d08" ← structural shape hash
},
"response": {
"status":201,
"headers": {"Content-Type":"application/json"},
"set_cookies": {},
"body": {"id":"ord_abc123","status":"created"},
"body_raw":"{\"id\":\"ord_abc123\",\"status\":\"created\"}",
"body_hash":"f44a1b"
},
"aws_meta": {"ecs": {"cluster":"prod","task_family":"my-app"}} ← runtime metadata
}
Body capture & parsing
- Up to 64 KiB captured per request and response body. Bodies exceeding this limit are truncated and flagged.
- JSON (
application/json) → structured body object with number types preserved.
- Form (
application/x-www-form-urlencoded) → key/value map in body.
- XML (
application/xml, text/xml, *+xml) → nested map with attributes prefixed @.
- Raw bytes always available in
body_raw alongside the parsed body field.
- On parse failure,
body falls back to the raw string.
Structural shape hashes
Each log event carries 6-character hex hashes (first 3 bytes of SHA-256) that capture the shape of the data — the set of keys and their types — independent of the actual values. Identical API call patterns produce identical hashes across all requests.
- queries_hash
- Shape of the URL query parameter map
- body_hash
- Shape of the request body (on
request)
- body_hash
- Shape of the response body (on
response)
- global_hash
- Combined shape of queries + req body + resp body
Use global_hash in CloudWatch Logs Insights to group all traffic with the same request/response structure, regardless of field values.
CloudWatch Logs
Async delivery with back-pressure
Log events are enqueued to an in-memory channel and flushed to CloudWatch by a
background goroutine. The proxy path never blocks on AWS API calls.
- Bounded queue of 1024 events. When full, excess events are dropped and counted; a warning is reported every 100 drops.
- Batches of up to 1000 events or 900 KiB per
PutLogEvents call.
- Background flush every 2 seconds and on shutdown.
- Each
PutLogEvents call is bounded to a 2-second timeout and retried up to 3× with 100 ms / 200 ms backoff on ThrottlingException and ServiceUnavailableException.
- Shutdown flushes the remaining queue within a 10-second budget before exit.
- Log stream name includes app name, PID, and Unix timestamp — unique across restarts.
- Log group and stream created at startup;
ResourceAlreadyExistsException is silently accepted.
Traffic logs → LOG_GROUP_NAME (default /app/log/{APP_NAME})
Health logs → HEALTH_LOG_GROUP_NAME (default /app/log/{APP_NAME}/health)
CloudWatch delivery is fully optional. If no AWS region can be resolved, or if AWS
config loading fails, cwproxy continues proxying and logging to stdout without interruption.
EMF Metrics
Metrics embedded in log events
CloudWatch Embedded Metric Format (EMF) data is attached to every log event.
CloudWatch extracts it as custom metrics automatically — no cloudwatch:PutMetricData permission needed.
Metrics are also emitted to stdout for local consumption.
Namespace: app/traffic
Dimensions: {AppName} and {AppName, Endpoint, Method}
RequestCountcount
Latencyms
RequestBodySizebytes
ResponseBodySizebytes
2XXStatusCodecount
4XXStatusCodecount
5XXStatusCodecount
Namespace: app/health
Dimensions: {AppName, Endpoint}
HealthStatuscount (0/1)
HealthLatencyms
Each request emits metrics under both {AppName} (aggregate) and
{AppName, Endpoint, Method} (per-route) dimension sets in a single log event.
Multiple EMF directives are emitted as separate lines when needed to stay within CloudWatch limits.
Required IAM permissions
logs:CreateLogGroup
logs:CreateLogStream
logs:PutLogEvents
Health Checks
Periodic probing with full telemetry
cwproxy runs concurrent HTTP GET probes against all configured
HEALTH_URLS at a fixed 30-second interval (configurable).
Health events are logged to their own log group with full request/response detail.
- All endpoints probed concurrently each interval.
- Probe fires immediately at startup, then on each tick.
- Captures up to 64 KiB of response body; logs parsed body in health events.
- Reports response headers and cookies in health log events.
HealthStatus=1 for HTTP 2xx–3xx; 0 for 4xx, 5xx, and transport errors.
- Traffic requests matching a health path are suppressed from traffic logs — they do not emit traffic metrics or log events.
- Health log events omit
_q and app_name fields; they retain _t, request/response payloads, metadata, and EMF.
HEALTH_URLS
Flexible URL resolution
Each comma-separated entry in HEALTH_URLS may omit any combination
of scheme, host, port, or path. Missing parts are filled from defaults.
- scheme
- Default:
http. https supported.
- host
- Default:
APP_HOST
- port
- First entry:
APP_PORT. Later http: 80. https: 443.
- path
- Later entries without a path inherit the first entry path.
/health
→
http://{APP_HOST}:{APP_PORT}/health
:9000/health
→
http://{APP_HOST}:9000/health
/healthz,alb.example.com
→
{APP_HOST}:{APP_PORT}/healthz
alb.example.com:80/healthz
https://alb.example.com/ping
→
https://alb.example.com:443/ping
Upstream Retry
Configurable policy applied inside the transport
Retries happen transparently inside the HTTP transport. Only the final upstream response
is logged and metered — intermediate failures are invisible to callers.
Set BACKEND_RETRY_MAX_ATTEMPTS=1 to disable retries entirely.
Policy parameters
- MAX_ATTEMPTS
- Total upstream attempts including the first. Default: 2.
- INITIAL_BACKOFF
- Delay before the first retry. Default: 50 ms.
- MAX_BACKOFF
- Upper bound on retry delay. Default: 250 ms.
- MULTIPLIER
- Exponential backoff multiplier. Default: 2.
- METHODS
- HTTP methods eligible for retry. Default: GET, HEAD, OPTIONS.
- STATUS_CODES
- Upstream status codes that trigger a retry. Default: 500–599. Supports ranges (
500-503) and individual codes.
- ON_TRANSPORT_ERRORS
- Retry on network-level failures in addition to status codes. Default: false. Context cancellation and deadline exceeded are never retried.
- BODY_BUFFER_BYTES
- Request bodies up to this size are buffered for replay. Larger bodies are not retried. Default: 64 KiB.
Retry eligibility rules
- Method must be in
BACKEND_RETRY_METHODS.
- Upgrade requests (WebSocket) and CONNECT are never retried.
- Bodies with
GetBody set are always replayable (e.g. requests from Go's HTTP client).
- Bodies without
GetBody are buffered up to BODY_BUFFER_BYTES. Bodies exceeding the limit are not retried.
- On a retryable status, the consumed upstream response body is drained and discarded before the next attempt.
- Context cancellation during the backoff wait returns the received response rather than an error.
Request body bytes captured in logs reflect all upstream attempts, not just the first — so RequestBodySize metrics count total bytes sent across retries.
Runtime Metadata
Automatic AWS environment enrichment
At startup, cwproxy collects instance, task, and pod metadata with a
200 ms global timeout. Available fields are attached as aws_meta
to every log event. Missing or inaccessible sources are omitted without error.
EC2
Instance Identity
Fetched from IMDSv2 with the configured 200 ms timeout.
instance_id
instance_type
region
availability_zone
image_id
account_id
ECS
Task & Container
Fetched from ECS_CONTAINER_METADATA_URI_V4 (v3 fallback).
cluster
task_arn
task_family
task_revision
launch_type
availability_zone
container_arn
container_name
EKS
Pod & Workload
Read from environment variables and the serviceaccount JWT claims.
cluster_name
deployment_name
namespace
pod_name
pod_uid
node_name
service_account
execution_environment
APP_NAME auto-detection order:
APP_NAME env var → EKS deployment name (inferred from pod name hash) → ECS task family → os.Hostname() → "cwproxy"
AWS_REGION auto-detection order:
AWS_REGION env → AWS_DEFAULT_REGION env → EC2 IMDS region → ECS task ARN region → AZ-derived region (strips last character)
Configuration
Environment variables
All configuration is via environment variables. Unset variables use the listed defaults.
| Variable |
Default |
Description |
| Core |
| PROXY_PORT | 8081 | Port that cwproxy listens on. |
| APP_HOST | 127.0.0.1 | Upstream application host. Also the default host for omitted HEALTH_URLS hosts. |
| APP_PORT | 8080 | Upstream application port. In inspector mode, overrides auto-detection when set. |
| APP_NAME | auto-detected | Application name used in logs and metric dimensions. See auto-detection order above. |
| Health |
| HEALTH_URLS | {APP_HOST}:{APP_PORT}/health | Comma-separated health probe endpoints. Partial URLs are resolved against APP_HOST/APP_PORT. |
| HEALTH_INTERVAL | 30s | Health probe interval. Must be at least 1s. |
| CAPTURE_BODY_LIMIT | 65536 | Maximum bytes captured per request or response body. Bodies exceeding this are truncated and flagged. Must be at least 1. |
| CloudWatch Logs |
| LOG_GROUP_NAME | /app/log/{APP_NAME} | CloudWatch Logs group for traffic logs and traffic EMF. |
| HEALTH_LOG_GROUP_NAME | /app/log/{APP_NAME}/health | CloudWatch Logs group for health logs and health EMF. |
| AWS_REGION | — | AWS region override. Takes precedence over all other region sources. |
| AWS_DEFAULT_REGION | — | Secondary AWS region override. Used when AWS_REGION is unset. |
| Upstream Retry |
| BACKEND_RETRY_MAX_ATTEMPTS | 2 | Total upstream attempts including the first. Set to 1 to disable retries. |
| BACKEND_RETRY_INITIAL_BACKOFF | 50ms | Delay before the first retry attempt. |
| BACKEND_RETRY_MAX_BACKOFF | 250ms | Upper bound on exponential backoff delay. |
| BACKEND_RETRY_BACKOFF_MULTIPLIER | 2 | Exponential backoff multiplier applied between retry attempts. |
| BACKEND_RETRY_METHODS | GET,HEAD,OPTIONS | Comma-separated HTTP methods eligible for retry. |
| BACKEND_RETRY_STATUS_CODES | 500-599 | Comma-separated upstream status codes or ranges (e.g. 500-503,429) that trigger a retry. |
| BACKEND_RETRY_ON_TRANSPORT_ERRORS | false | When true, also retry on network-level upstream errors. Context cancellation and deadline exceeded are never retried. |
| BACKEND_RETRY_BODY_BUFFER_BYTES | 65536 | Maximum request-body bytes buffered for replay on retry. Requests with larger bodies are not retried. |
Operational Safety
Built for unattended operation
- ✓Panic recovery wraps every request handler goroutine and the health runner. A panic in one request returns 500 to the client and logs the recovered value; it never takes down the process.
- ✓Bounded body capture — body buffers have a hard 64 KiB ceiling. Memory use cannot grow unboundedly with large payloads.
- ✓Graceful shutdown — on SIGTERM or SIGINT, the server drains in-flight requests, then flushes the CloudWatch queue within a 10-second budget.
- ✓CloudWatch degradation — AWS config and sink initialization failures are logged to stderr; the proxy continues serving traffic and logging to stdout without interruption.
- ✓X-Forwarded-For preservation — inbound XFF headers from upstream load balancers are passed through and X-Forwarded-* is appended correctly.
- ✓No unbounded goroutine growth — the CloudWatch queue, batch sizes, and process tree walk are all bounded.
Deployment
Static binary, no runtime deps
Binaries are built with CGO_ENABLED=0. Drop them anywhere.
Platforms
linux/amd64
linux/arm64
darwin/amd64
darwin/arm64
windows/amd64
Container
ghcr.io/awsutils/cwproxy:latest
Alpine base · non-root user · CA certs included
Deployment patterns: beside a systemd service on bare metal, as a Kubernetes sidecar container (same Pod network namespace), or in host-network Docker with the app on loopback.
# verify download integrity
sha256sum -c SHA256SUMS
# Docker sidecar
docker run --rm --network host \
-e APP_PORT=8080 -e PROXY_PORT=8081 \
-e APP_NAME=my-app -e AWS_REGION=us-east-1 \
ghcr.io/awsutils/cwproxy:latest
How To Use
Quick start guide
-
Download the binary matching your OS and architecture from the section below.
On Linux or macOS, make it executable:
chmod +x ./cwproxy-linux-amd64
-
Configure via environment variables and start alongside your app in normal mode:
export PROXY_PORT=8081
export APP_PORT=8080
export APP_NAME=my-app
export HEALTH_URLS=/health
export LOG_GROUP_NAME=/app/log/my-app
export HEALTH_LOG_GROUP_NAME=/app/log/my-app/health
export AWS_REGION=us-east-1
./cwproxy-linux-amd64
-
Or use inspector mode to let
cwproxy launch and monitor the target process:
./cwproxy-linux-amd64 node ./server.js
-
Send traffic to
PROXY_PORT. Structured JSON logs appear on stdout immediately.
CloudWatch delivery begins as soon as AWS credentials are resolved (instance profile, env vars, or ~/.aws/credentials).
Downloads
Published artifacts
Choose the binary for your OS and architecture.
Verify integrity with SHA256SUMS before running.
On Linux and macOS, run sha256sum -c SHA256SUMS.
On Windows PowerShell, use Get-FileHash and compare manually.
Checksum manifest for every published binary.
Statically linked release binary.
Statically linked release binary.
Statically linked release binary.
Statically linked release binary.
Statically linked release binary.