Home → Blog

You need a reliable way to quantify current congestion at the Port of Savannah without building your own AIS pipeline. By the end of this guide, you will be able to query and interpret Savannah’s congestion signal using FreightPulse’s IMF PortWatch feed, integrate it into code, and ship an alert or dashboard that uses a 7-day average versus a 90-day baseline—without mistaking it for live vessel wait times.

What Savannah “congestion” measures (and what it doesn’t)

FreightPulse exposes a congestion indicator that compares the last 7 days’ average port calls to a 90-day baseline for the same port. This comes from IMF PortWatch, which aggregates UN Global Platform AIS data. The feed updates daily and is generally lagged by about 6–7 days. It is a robust, comparable trend signal across time.

Illustration: Savannah Port Congestion

It is not a live AIS anchorage or berth-wait metric, does not provide minute-by-minute queue lengths, and it should not be used as a same-day operational dispatch signal. Treat it as a statistically smoothed port traffic indicator, useful for supply chain planning, capacity-risk monitoring, and routing heuristics that tolerate a few days of data latency.

Quick facts for implementers

How to query Savannah congestion

Use a simple GET request with your API key in the X-API-Key header. The port parameter for Savannah is USSAV. Responses are wrapped in a top-level success flag and a data object containing results, plus a meta block that describes the methodology and data lag.

If you don’t provide a key, the service allows a guest quota (100 requests/day per IP), suitable for light prototyping. For production and higher frequency polling, register for your own key.

cURL request

Copy and run this call as-is (replace YOUR_API_KEY with your token if you have one):

curl -H "X-API-Key: YOUR_API_KEY" "https://freightpulsehq.com/api/v1/port-congestion?port=USSAV"

Official JSON response

Below is the official sample for USSAV. Use it to shape your parser and unit tests.

{
"success": true,
"data": {
"results": [
{
"port": "Savannah",
"locode": "US SAV",
"country": "United States",
"congestion": "moderate",
"as_of": "2026-10-02",
"metrics": {
"recent_avg_portcalls_per_day": 5.9,
"baseline_avg_portcalls_per_day": 5.9,
"ratio_vs_baseline": 0.99
}
}
],
"global_summary": {
"ports_high_congestion": 0,
"ports_moderate": 1,
"ports_low": 0
},
"meta": {
"source": "IMF PortWatch (portwatch.imf.org) - UN Global Platform AIS data",
"data_lag_days": "~6-7 (daily updates, not real-time)",
"methodology": "congestion level is an approximation: last 7 days' avg port calls vs 90-day baseline for the same port. Does not measure vessel wait/anchorage time."
}
},
"meta": {
"request_id": "82989f45-8c00-4078-9a0c-fba1d369456f"
}
}

Fields to read for operational logic:

Interpreting the Savannah signal

For USSAV, you will typically compare ratio_vs_baseline against internal thresholds to trigger routing checks or capacity warnings. A ratio near 1.0 suggests activity is close to its recent baseline, while materially lower or higher values can indicate atypical slowdowns or surges in port calls. The congestion band condenses this into a discrete label for fast triage, but you should keep the underlying ratio for alert tuning and backtesting.

Remember that “congestion” here is derived from port-calls volume, not berth occupancy or anchorage wait. Port calls can be affected by schedule changes, blank sailings, and seasonality. Use the ratio as one feature among others (e.g., terminal advisories, inland dray capacity signals) if your workflow is sensitive to same-week volatility.

Python: fetch, parse, and act on USSAV congestion

The snippet below requests USSAV congestion, extracts the key fields, and demonstrates a minimal decision rule. It also sketches a daily cache pattern to avoid redundant requests given the data’s cadence.

import os
import json
import time
import requests

API_URL = "https://freightpulsehq.com/api/v1/port-congestion"
PORT_CODE = "USSAV"
API_KEY = os.getenv("FREIGHTPULSE_API_KEY", "YOUR_API_KEY") # replace in production

# Simple daily cache (file-based). Data is lagged ~6-7 days and updates daily.
CACHE_FILE = "ussav_congestion_cache.json"
CACHE_TTL_SECONDS = 12 * 60 * 60 # refresh at most twice per day

def load_cache():
if not os.path.exists(CACHE_FILE):
return None
try:
mtime = os.path.getmtime(CACHE_FILE)
if time.time() - mtime > CACHE_TTL_SECONDS:
return None
with open(CACHE_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return None

def save_cache(payload):
try:
with open(CACHE_FILE, "w", encoding="utf-8") as f:
json.dump(payload, f)
except Exception:
pass

def fetch_ussav_congestion():
headers = {"X-API-Key": API_KEY}
params = {"port": PORT_CODE}
resp = requests.get(API_URL, headers=headers, params=params, timeout=20)
resp.raise_for_status()
return resp.json()

def extract_fields(doc):
# Expecting the official envelope: {"success": true, "data": {...}}
if not doc.get("success"):
raise ValueError("API request did not succeed")
results = doc.get("data", {}).get("results", [])
if not results:
raise ValueError("No results for USSAV")
r0 = results[0]
congestion_band = r0.get("congestion")
as_of = r0.get("as_of") # ISO date (lagged ~6-7 days)
metrics = r0.get("metrics", {})
ratio = metrics.get("ratio_vs_baseline")
recent_avg = metrics.get("recent_avg_portcalls_per_day")
baseline_avg = metrics.get("baseline_avg_portcalls_per_day")
return {
"congestion": congestion_band,
"as_of": as_of,
"ratio_vs_baseline": ratio,
"recent_avg": recent_avg,
"baseline_avg": baseline_avg,
}

def compute_advice(ratio, congestion_band):
# Example rule: flag checks if ratio deviates materially from baseline
if ratio is None:
return "no_data"
if ratio < 0.85:
return "risk_downturn_check_routing"
if ratio > 1.15:
return "surge_monitor_terminal_capacity"
# Optionally incorporate the qualitative band for human-facing messages
return f"normal_monitoring_{congestion_band or 'unknown'}"

def main():
cached = load_cache()
if cached:
doc = cached
else:
doc = fetch_ussav_congestion()
save_cache(doc)

fields = extract_fields(doc)
action = compute_advice(fields["ratio_vs_baseline"], fields["congestion"])

print(json.dumps({
"port": "USSAV",
"as_of": fields["as_of"],
"congestion": fields["congestion"],
"ratio_vs_baseline": fields["ratio_vs_baseline"],
"action": action
}, indent=2))

if __name__ == "__main__":
main()

Notes for production:

Dashboards and alerting patterns that work for USSAV

As a general pattern, persist the last N daily observations for USSAV and compute week-over-week and month-over-month comparisons. The provided ratio_vs_baseline already normalizes for recent history, but tracking its drift helps you detect inflection points earlier than waiting for a band transition from, say, moderate to high.

To minimize alert fatigue, prefer banded alerts augmented with a hysteresis margin on the ratio. For example, do not clear an alert until the ratio crosses back through a tighter threshold neighborhood, or require two consecutive daily updates to confirm a state change. Because the “as_of” date is daily and lagged, the maximum alert resolution is daily—avoid intra-day checks for this specific signal.

Field references and units

There is no currency field in this endpoint. Do not attempt to infer freight rates from this data, and do not present any rate in USD as “today’s price” on the basis of a congestion readout.

Error handling and envelope parsing

Always check the top-level success flag before reading data.results. If success is false or data is missing, handle it as a soft failure and preserve the last good observation in your cache. For observability, log the meta.request_id so you can correlate issues with support if needed.

The endpoint returns an array under data.results. For single-port queries like USSAV, parse the first element. If you later query multiple ports, iterate this array and store each record keyed by its port or UN/LOCODE.

When to prefer the band vs. the ratio

Use the congestion band for human-facing UIs and quick filters. It communicates a state effectively (e.g., “moderate”) and aligns well with triage workflows. Use ratio_vs_baseline for automated decision rules and analytics. The continuous ratio allows you to set custom thresholds, compare across time windows, and perform backtests on alert precision/recall.

For Savannah specifically, baselining via the ratio helps account for seasonal shifts in scheduled port calls. If your planning horizon is weekly, align your alert logic to weekly cadences and refresh your cache once or twice per day.

Security, quotas, and deployment hygiene

End-to-end checklist for USSAV integration

FAQ

Is this live AIS wait-time data?
No. It is an IMF PortWatch signal based on AIS-derived port calls, published daily with a ~6–7 day lag. It does not measure anchorage or berth waiting times.

How often should I refresh Savannah’s congestion?
Daily is appropriate. The underlying dataset is updated daily and lagged; polling more frequently yields little benefit. Cache and serve internally to minimize calls.

Which fields should I store?
At minimum: congestion, metrics.ratio_vs_baseline, as_of, and optionally recent_avg_portcalls_per_day and baseline_avg_portcalls_per_day for diagnostics and dashboards.

What happens without an API key?
Guests can call the endpoint up to 100 requests/day per IP. For sustained usage and production, include X-API-Key with your token.

Can I use this to quote freight rates today?
No. This endpoint provides a congestion indicator, not prices. Do not present any freight rate as “today’s price” based on this signal.

Spin up your Savannah congestion integration in minutes: get an API key and read the full endpoint reference. Register for access, then review the Documentation for field definitions and usage patterns.