Every on-premises data migration hits the same wall, and it is not technical. It is the network team saying no.
The database sits in a data centre. It has no public IP, and it is not getting one. There is no VPN to the cloud, and building one is a quarter-long project with three teams. Meanwhile you need the table in Snowflake this month.
Snowflake's Data Connectivity Proxy (DCP) exists for exactly this position. It is one of several ways to reach a private source, and it is worth being precise about which problem it solves better than the alternatives.
The options, and where each one breaks
| Path | Inbound firewall change | Who runs the data plane | Fits when |
|---|---|---|---|
| Public endpoint + IP allowlist | Yes | Snowflake | the source is already internet-facing |
| VPN or Direct Connect + PrivateLink | A network project | Snowflake | it already exists and many workloads share it |
| Openflow BYOC / self-hosted deployment | No | You | you want the compute inside your own perimeter |
| Push model: an ETL tool writes into Snowflake | No | You | you accept operating an ETL stack |
| DCP | No | Snowflake | no inbound change is possible, and you want Snowflake to run the data plane |
DCP is not "the easy one". The honest differentiator is narrower and more useful: it is the only option that gives you a Snowflake-managed data plane with zero inbound exposure and no VPN. If you already have PrivateLink, use it. If you are happy running the data plane yourself, BYOC is a fine answer. DCP wins when both of those are off the table.
What DCP is
You run a lightweight agent on your network. The agent opens a single outbound TLS connection on port 443 to Snowflake and holds it open. Snowflake routes database traffic back down that tunnel.
The agent is a TCP-level pass-through. It does not parse, buffer, or store the payload, and the database credentials never leave Snowflake.
Architecture: two planes, one direction
Two separate paths matter here.
The control plane (dcp.<account>...) handles bootstrap, certificate issuance and rotation, and pushing the routing policy down to the agent. The data plane (dcp-proxy.<account>..., plus a fallback host) carries the actual database bytes.
Practical consequence: allow all three hostnames. If you allow only the fallback host, the agent still starts and everything still works, but every connection quietly takes the degraded path, and nothing tells you.
The trust boundary
This is the part a security reviewer will actually read, so state it plainly:
- The agent is outbound only. It opens no listening socket for data.
- It is a byte pass-through. No parsing, no buffering, no storage of the payload.
- Credentials stay in Snowflake, in a secret or a connector parameter. The agent host never holds the database password.
- Transport is mutual TLS with a client certificate issued by the Snowflake control plane. Operational certificates rotate on roughly a 30-day cycle, independently of the bootstrap token.
Two things to be honest about on the other side of that boundary. The agent host holds a bootstrap JWT valid up to 90 days, and anyone with that file can register an agent against your account until it expires — treat it as a rotating secret, not a config value. And the agent's Prometheus endpoint has no authentication and publishes exactly when your credential expires, so bind it to loopback and scrape it locally.
The idea that makes it click: routing is destination-driven
There is no tunnel setting on the connector. No proxy field. Nothing about DCP appears in the connector configuration at all — you configure it as if the database were directly reachable.
Snowflake never resolves db.internal. The hostname is matched as a string against a network rule; the agent resolves it, on your network, where that name means something.
That single design choice explains most of the rules. A hostname with no public DNS record works fine. A raw IP address is rejected, because there is nothing to resolve on the far side. And you never point a connector at a tunnel — you describe a destination, and the relay figures out which agent owns it.
Benefits, phrased the way your network team will hear them
Skip the feature bullets. Here is the actual ask:
One process on one host, making outbound connections to four fixed hostnames on TCP 443. No inbound rules. No listening ports. No public IP on the database. No VPN.
Two caveats belong in the same sentence, because getting them wrong wastes a deployment window:
- It needs a direct-egress exception, not an open port. The agent must reach the DCP hostnames itself. That is a bypass for a handful of FQDNs on 443. Everything else on the host can keep using the corporate proxy.
- No TLS inspection on that path. DCP authenticates with mutual TLS using a client certificate issued by Snowflake. A proxy that terminates TLS and re-signs with an internal CA cannot reproduce that certificate, so the handshake fails by design. A transparent pass-through proxy is fine. A decrypting one is not.
That second point is the one most often missed, and it is not a bug to work around. It is the security property working.
Setup shape
Five steps, in this order. Syntax skeletons only.
SELECT SYSTEM$ISSUE_PER_ACCOUNT_CERTIFICATES(); -- once per account, then wait about 30 minutes
CREATE DATA CONNECTIVITY PROXY <name> ENABLED = TRUE;
SELECT SYSTEM$GENERATE_DATA_CONNECTIVITY_PROXY_BOOTSTRAP_TOKEN('<name>', <days>);
CREATE NETWORK RULE <db>.<schema>.<rule>
MODE = DATA_CONNECTIVITY_PROXY_EGRESS -- plain EGRESS is silently ignored
TYPE = HOST_PORT
VALUE_LIST = ('<host>:<port>'); -- hostname, never an IP
CREATE EXTERNAL ACCESS INTEGRATION <eai>
ALLOWED_NETWORK_RULES = (<rule>) ENABLED = TRUE;
ALTER DATA CONNECTIVITY PROXY <name>
SET EXTERNAL_ACCESS_INTEGRATIONS = (<eai>); -- the step everyone skips
Then, spin up the docker image for the dcp-client
docker run -d snowflakedb/dcp-client:latest \
--sf-bootstrap-credentials <token-path> \
--metrics-addr 127.0.0.1:9092
Then verify before touching the connector:
SELECT * FROM TABLE(INFORMATION_SCHEMA.DCP_CLIENT_ROUTE_CHECK(
AGENT_ID => '<name>', DESTINATION => '<host>:<port>'));
Four traps, all of which cost me time during this exploration:
- The certificate call is asynchronous. Start the agent inside the 30-minute window and it cannot get its mTLS pair, and the error does not say so.
- Only
ALTER DATA CONNECTIVITY PROXY ... SET EXTERNAL_ACCESS_INTEGRATIONSassociates an integration with the proxy. Creating the integration does not. Attaching it to the Openflow runtime does not either — that is a separate, also-required step. -
MODE = EGRESSis accepted and then not applied to DCP. The symptom looks like a routing bug. - Never
CREATE OR REPLACEa network rule or an external access integration. It silently detaches from every runtime referencing it and breaks unrelated connectors with no error.
Limitations
DCP is still early, and the boundaries are sharp. Know them before you design around it.
| Limitation | What it means for you |
|---|---|
| Serves Openflow only today | Other Snowflake services are planned, not available. The consumer is always an Openflow connector. |
| No corporate forward HTTP proxy | The agent needs direct egress. There is no proxy configuration to set. |
| No PrivateLink for the agent | The agent must reach the public DCP hostnames. Your account can still use PrivateLink for inbound. |
| No active-active load balancing | Two agents cannot share load to the same destination. Split destinations across agents instead. |
| Agent upgrades are not hitless | Replacing the image tears down active TCP sessions. It drains on SIGTERM, so allow a graceful stop. Relay-side upgrades are transparent. |
| Connection resets are visible | If an agent fails, its tunnels break. DCP does not hide this — the consumer needs reconnect logic. |
| One proxy object per network scope | Sources in isolated networks each need their own agent and their own DCP object. |
On observability, set expectations honestly. The per-stage route check is the best single diagnostic, but only its first six stages are informative — I saw the last two sit at IN_PROGRESS with the connector live and connections counted, and the connection-history table function returned a single placeholder row throughout. The agent's own OTLP metrics (agent_cp_connected, agent_connections_total, agent_destination_errors_total) are the reliable signal.
Cost and latency
DCP itself introduces no billable compute object. The cost sits in the Openflow runtime and in the warehouse that applies changes.
For a CDC pipeline, that split matters: capture runs continuously and uses no warehouse, writing to a journal table. Only the merge, on its schedule, spends warehouse credits. With nothing to merge, the warehouse suspends and idle costs nothing.
Latency is the counter-intuitive part. Round-trip time to the relay barely matters, because CDC holds long-lived connections, so the TLS setup cost is paid once and amortises away. Freshness is governed by your merge schedule, not by network distance. Tune the schedule first. Treat RTT as a cold-start and throughput concern.
For throughput: the agent is I/O-bound, not CPU-bound. Encryption is not the bottleneck, host network bandwidth is. Scale by adding agents and splitting destinations across integrations, not by buying a bigger host.
When not to use DCP
- A one-off ad-hoc query against an on-prem database. This is a pipeline mechanism.
- Any consumer that is not Openflow, today.
- A destination with no DNS name and no way to add a hosts entry on the agent host.
- An environment that mandates TLS inspection on all egress with no exception path. That is a hard stop, not a workaround.
Is it worth it?
If your blocker is organisational rather than technical — the network team will not open inbound, and a VPN is a quarter away — DCP collapses a network project into an outbound exception for four hostnames. Run at least two agents in separate failure domains, alert on bootstrap-token expiry, and it is a reasonable production pattern.
Just design with the limitations table in front of you, not after.
Verified on Snowflake as of September 2026, on an Openflow gen 1 runtime replicating PostgreSQL. The feature is evolving, so check the current documentation for what has shifted.


Top comments (0)