Heroku Static IP: Fixed Outbound IPs for Dynos
Heroku dynos in the Common Runtime use dynamic outbound IPs that change as dynos restart, and Heroku has no static IP option outside Private Spaces. Set a Fixedmark proxy URL as a config var, route allowlisted calls through it, and the same dedicated pair keeps working if you later move off Heroku.
Why Heroku dynos change IP
Heroku restarts every dyno about once every 24 hours, plus on every deploy and config var change. A new dyno can run on a different host, and in the Common Runtime outbound traffic leaves from a highly dynamic pool of AWS addresses. Heroku's own help article recommends a static IP add-on for allowlisting.
Heroku's native option and the add-on market
Heroku offers stable outbound IPs only in Private Spaces. Per Heroku's docs, all outbound traffic from a Private Space leaves from a small, stable list of IPs dedicated to the space, and Private Spaces are available only to verified Heroku Teams and Heroku Enterprise accounts. Most teams on the Common Runtime use a proxy add-on instead. Fixie and QuotaGuard Static are the long-standing options, both billed through Heroku, with paid plans from $5 per month.
An add-on is the right choice if you want Heroku to handle billing and you plan to stay. Fixedmark is direct sign-up, not an add-on, and is aimed at teams that want dedicated IPs or are planning a migration. See Fixie alternative and QuotaGuard alternative for sourced comparisons.
Sources for Heroku pricing
Checked October 2026. Confirm current details on the vendor's site before you decide. Prices and limits change.
- Heroku Dev Center: Private Spaces
- Heroku Help: IP ranges for allowlisting dynos
- Heroku Dev Center: Dyno restarts
- Heroku Elements: Fixie add-on
- Heroku Elements: QuotaGuard Static add-on
Set up a static IP on Heroku
Dynos are long-running processes, so both the HTTPS proxy and SOCKS5 or tunnel paths work.
Fixedmark is in early access. Proxy URLs are issued at launch. Join early access to get yours when your region opens.
Before you start
- A Heroku app and the Heroku CLI, logged in.
- A Fixedmark proxy URL (
https://APP_ID:TOKEN@mum.egress.fixedmark.com:443) and the two IPs of your pair. Both are issued at launch. Join early access to get them. - For databases: the SOCKS5 URL (
socks5h://APP_ID:TOKEN@mum.egress.fixedmark.com:1080) or thebm tunnelCLI. - Access to the partner's or database's allowlist, or a contact who can add two IPv4 addresses for you.
Steps
- 1Set the config varRun
heroku config:set FIXEDMARK_PROXY_URL=https://APP_ID:TOKEN@mum.egress.fixedmark.com:443 -a your-app. Heroku restarts the dynos with the new value. - 2Configure the HTTP clientPass the proxy URL to the client that calls the allowlisted API. For Ruby, use Typhoeus with
proxy:set to thehttps://URL.Net::HTTPand Faraday's default adapter cannot tunnel HTTPS through a TLS-wrapped proxy, so they fail even onnet-http0.5+. See the Ruby guide. - 3If you are replacing an add-on, run both brieflyAsk the partner to add the Fixedmark pair next to the old IPs. Switch the client, watch connection logs, then remove the old IPs and the add-on.
- 4VerifyRun the one-off dyno check below. It should print one of your Fixedmark IPs.
// npm install undici (undici 8 needs Node.js 22.19+)
import { fetch, ProxyAgent } from "undici";
// FIXEDMARK_PROXY_URL=https://APP_ID:TOKEN@mum.egress.fixedmark.com:443
const proxy = new ProxyAgent(process.env.FIXEDMARK_PROXY_URL);
const res = await fetch("https://api.partner.example/v1/orders", {
dispatcher: proxy,
headers: { authorization: `Bearer ${process.env.PARTNER_API_KEY}` },
});
console.log(res.status, await res.json());# pip install requests (urllib3 1.26+ for an https:// proxy URL)
import os
import requests
proxy = os.environ["FIXEDMARK_PROXY_URL"]
proxies = {"http": proxy, "https": proxy}
res = requests.get(
"https://api.partner.example/v1/orders",
proxies=proxies,
headers={"Authorization": f"Bearer {os.environ['PARTNER_API_KEY']}"},
timeout=15,
)
print(res.status_code, res.json())package main
import (
"fmt"
"net/http"
"net/url"
"os"
"time"
)
func main() {
proxyURL, err := url.Parse(os.Getenv("FIXEDMARK_PROXY_URL"))
if err != nil {
panic(err)
}
client := &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
Timeout: 15 * time.Second,
}
res, err := client.Get("https://api.partner.example/v1/orders")
if err != nil {
panic(err)
}
defer res.Body.Close()
fmt.Println(res.Status)
}SDKs that do not use fetch (the Stripe and Salesforce Node SDKs, got, axios) accept an HTTP agent. Pass new HttpsProxyAgent(process.env.FIXEDMARK_PROXY_URL) from the https-proxy-agent package, which supports https:// proxy URLs. With axios, also set proxy: false so it does not apply its own proxy logic. undici 8 needs Node 22.19 or later. On older Node, install undici@7.
Verify the egress IP on Heroku
Check the IP before you send it to a partner. heroku run 'curl -sS --proxy "$FIXEDMARK_PROXY_URL" https://fixedmark.com/api/ip' -a your-app runs the check in a one-off dyno with your config vars. The endpoint https://fixedmark.com/api/ip returns the caller's IP as plain text, and the What's my IP tool shows the same thing in a browser.
The proxied call should print one of your two Fixedmark IPs every time. The direct call should print a different address. If both print the same platform IP, the client is not using the proxy.
# Needs curl 7.52 or later for an https:// proxy URL.
# Prints the IP the destination sees. Run it a few times:
# you should only ever see the two IPs of your pair.
curl -sS --proxy "$FIXEDMARK_PROXY_URL" https://fixedmark.com/api/ip
# Same check over SOCKS5, the path database drivers use.
curl -sS --proxy "$FIXEDMARK_SOCKS_URL" https://fixedmark.com/api/ip
# Without the proxy, for comparison. This is the platform's own IP.
curl -sS https://fixedmark.com/api/ip// A temporary route or script that reports the egress IP.
// Remove it after you have checked.
import { fetch, ProxyAgent } from "undici";
const proxy = new ProxyAgent(process.env.FIXEDMARK_PROXY_URL);
const viaProxy = await fetch("https://fixedmark.com/api/ip", { dispatcher: proxy });
const direct = await fetch("https://fixedmark.com/api/ip");
console.log({
fixedmark: (await viaProxy.text()).trim(),
platform: (await direct.text()).trim(),
});import os
import requests
proxy = os.environ["FIXEDMARK_PROXY_URL"]
via = requests.get("https://fixedmark.com/api/ip", proxies={"https": proxy}, timeout=10)
direct = requests.get("https://fixedmark.com/api/ip", timeout=10)
print({"fixedmark": via.text.strip(), "platform": direct.text.strip()})Databases: Postgres, MySQL, and MongoDB
Database drivers do not speak HTTP CONNECT, so the proxy URL above does not cover them. On Heroku you have two options. Drivers with SOCKS5 support can use FIXEDMARK_SOCKS_URL directly: the MongoDB Node.js driver accepts proxyHost and proxyPort options, and Go's pgx accepts a SOCKS5 dialer. For any other driver, run the bm tunnel CLI (planned for launch) in your Procfile before the app, for example web: bm tunnel 5432:db.example.com:5432 & bundle exec puma, then point the driver at 127.0.0.1.
TLS to the database stays end to end. Fixedmark forwards encrypted bytes and never sees your queries. If your driver verifies the server certificate, keep the real database hostname as the TLS server name when you connect through the local tunnel.
- MongoDB Atlas: add both Fixedmark IPs to the project's IP Access List. See MongoDB Atlas allowlisting.
- AWS RDS, Cloud SQL, and self-hosted Postgres or MySQL: add both IPs as /32 entries. See Postgres and MySQL allowlists.
# Forward local port 5432 to the database through your Fixedmark IPs.
# bm tunnel is planned for launch; flags are not final.
bm tunnel 5432:db.example.com:5432 &
# Point the driver at the local end of the tunnel.
DATABASE_URL="postgres://app:secret@127.0.0.1:5432/app?sslmode=require"// npm install mongodb socks
import { MongoClient } from "mongodb";
// FIXEDMARK_SOCKS_URL=socks5h://APP_ID:TOKEN@mum.egress.fixedmark.com:1080
const socks = new URL(process.env.FIXEDMARK_SOCKS_URL);
const client = new MongoClient(process.env.MONGODB_URI, {
proxyHost: socks.hostname,
proxyPort: Number(socks.port),
proxyUsername: decodeURIComponent(socks.username),
proxyPassword: decodeURIComponent(socks.password),
});
await client.connect();import (
"context"
"net/url"
"os"
"github.com/jackc/pgx/v5/pgxpool"
"golang.org/x/net/proxy"
)
func connect(ctx context.Context) (*pgxpool.Pool, error) {
u, err := url.Parse(os.Getenv("FIXEDMARK_SOCKS_URL"))
if err != nil {
return nil, err
}
pw, _ := u.User.Password()
dialer, err := proxy.SOCKS5("tcp", u.Host,
&proxy.Auth{User: u.User.Username(), Password: pw}, proxy.Direct)
if err != nil {
return nil, err
}
cfg, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
if err != nil {
return nil, err
}
cfg.ConnConfig.DialFunc = dialer.(proxy.ContextDialer).DialContext
return pgxpool.NewWithConfig(ctx, cfg)
}Common errors and fixes
Requests still leave through Fixie or QuotaGuard
Those add-ons set their own config vars (FIXIE_URL, QUOTAGUARDSTATIC_URL) and some apps read them by default. Make sure the client reads FIXEDMARK_PROXY_URL, and that no global HTTP_PROXY points at the old add-on.
407 Proxy Authentication Required
The proxy did not get valid credentials. Check that the env var holds the full URL with APP_ID:TOKEN@, with no quotes or trailing newline. If the token contains @, :, or /, URL-encode it. Some clients drop credentials from the URL: pass them separately (Deno's basicAuth, axios proxy.auth) or switch to undici's ProxyAgent, which reads them from the URL.
TLS errors to the proxy: wrong version number, EPROTO, Proxy CONNECT aborted, or unable to connect to proxy
The client does not support an https:// proxy URL and is speaking plain HTTP to port 443. Upgrade it: curl 7.52+, urllib3 1.26+ (requests), httpx 0.26+, undici ProxyAgent, Go 1.10+. In Ruby, use Typhoeus: Net::HTTP and Faraday's default adapter cannot tunnel HTTPS through a TLS proxy. For older axios releases, set proxy: false and pass https-proxy-agent as httpsAgent. Java's built-in HttpClient cannot speak TLS to a proxy at all. If you cannot upgrade, use the SOCKS5 URL.
ETIMEDOUT or ECONNRESET to the partner, or a 403 from the partner
The proxy connected, but the destination dropped you. Usually only one IP of the pair is on the allowlist, or the partner has not applied the change yet. Add both IPs. If a destination allowlist is set on your Fixedmark token, check the host and port are on it.
The IP check prints the platform's IP, not a Fixedmark IP
The request went direct. Node's built-in fetch ignores HTTPS_PROXY unless NODE_USE_ENV_PROXY=1 is set (Node 22.21+ and 24+). Many SDKs ignore proxy env vars too. Pass the proxy to the client explicitly, as in the snippets above.
429 or 407 with an X-Proxy-Error: quota_exceeded header
Planned behavior when you pass your plan's soft limit. Check usage in the dashboard or raise the plan.
Database driver has no proxy option
node-postgres, mysql2, psycopg, and most ORMs cannot use an HTTP proxy. Use a SOCKS5-capable driver with FIXEDMARK_SOCKS_URL, or run bm tunnel where the platform allows a second process.
Gotchas on Heroku
Heroku Postgres and other add-ons reached over Heroku's network do not need the proxy. Route only external, allowlisted destinations.
- Fixedmark is not listed in the Heroku Elements marketplace at launch. Billing is direct.
- A
bm tunnelprocess in the Procfile restarts with the dyno, so it survives the daily restart. - Inbound static IPs are on the Fixedmark roadmap.
Related guides
- Use caseHeroku migrationMove off Heroku without breaking partner allowlists by running both IP sets in parallel during the switch.
- ComparisonFixie alternativeFixie's published plans and regions next to Fixedmark's planned dedicated IP pairs, Asia regions, and single HTTP plus SOCKS5 plan.
- ComparisonQuotaGuard alternativeQuotaGuard Static and Shield published plans next to Fixedmark's planned $29 dedicated pair.
- IntegrationRenderAllowlist two IPs that are yours instead of Render's shared regional ranges.
Frequently asked questions
Does Heroku provide static IPs?
Only in Private Spaces, which need a verified Heroku Teams or Heroku Enterprise account. Common Runtime dynos use dynamic AWS addresses, so most teams use a proxy add-on or a static IP proxy for fixed egress.
How often does a Heroku dyno's IP change?
Dynos restart about once every 24 hours, plus on every deploy and config var change. Each restart can land on a host with a different outbound IP.
Can I keep my static IP if I leave Heroku?
With Fixedmark, yes. The IP pair belongs to your Fixedmark account, so you change only where the app runs. Check with your current add-on vendor whether its IPs can move with you.
Is Fixedmark a Heroku add-on?
Not at launch. You sign up directly and set the proxy URL as a config var.
Fix your egress IP.
Fixedmark is in early access. Join the list to get a dedicated IP pair for Heroku when your region opens.