Strait of Hormuz Monitor

Offline

Methodology

This page documents the rules behind every number on the dashboard so the data is reproducible and the caveats are explicit. Each metric links to the code path that produces it.

Zone boundaries

  • Strait narrows: lat 25.8–26.8°N, lon 56.0–56.8°E. Vessels inside this rectangle are either transiting or anchored in the inshore channel.
  • Persian Gulf side: lon < 56.0°E. Includes Ras Tanura, Jubail, Kharg, Bandar Abbas, Jebel Ali.
  • Gulf of Oman side: lon > 56.8°E or (lat < 25.8°N and lon ≥ 56.0°E). Includes Fujairah, Khor Fakkan, Sohar.

Motion classification

A vessel is "in transit" only if we observe sustained motion, not a single fast ping:

  • Latest AIS speed-over-ground ≥ 3 knots, AND
  • Either (a) ≥ 2 narrows observations in the last 30 min all at ≥ 3 kn, (b) longitude travel > 0.03° (~3 km) between oldest and newest narrows pings, or (c) a single ping at ≥ 6 kn.

Vessels physically inside the narrows but below the motion gate are bucketed as anchored in narrows and attributed to the side they arrived from. This filters tugs, pilot boats, and circling support craft.

Transit counts (exited / entered)

A crossing is a vessel path whose endpoints are in opposite gulfs: persian_gulf → … → gulf_of_oman (exit) or gulf_of_oman → … → persian_gulf (entry). The in-between can include narrows hops and does not need a specific shape. Hops are persisted to the vessel_transit table; per-vessel chains are built by coalescing consecutive hops whose to-zone matches the next from-zone within a 24-hour gap, and a terminal hop is inferred from the vessel's most-recent AIS position when it falls in a different zone than the last recorded hop.

A vessel that dipped into the narrows from one side and returned to the same side (Oman → narrows → Oman) is not counted — both endpoints are on the Oman side. A vessel currently still in the narrows mid-transit is not counted either (no terminal side yet).

The wide-window counts (exited_gulf / entered_gulf) cover the full position-scan window (7 days by default). The strict 24 h subset is exposed as exited_gulf_24h /entered_gulf_24h. throughput_pct uses the 24 h count; the wide count drives the "X crossings observed in the last N days" status detail when the 24 h window is empty.

Satellite AIS polling happens every 2–4 hours. Vessels broadcasting intermittently can cross without both pre- and post-crossing pings being captured — such transits are undercounted. Terrestrial AIS coverage near the narrows mitigates this.

Petroleum-carrier classification

A vessel is counted as a petroleum carrier when any of the following is true — a union of signals, permissive by design:

  1. vessel_static.cargo_type is crude_oil orlng (from the Data Docked enrichment API).
  2. vessel_static.ship_type_specific contains "tanker", "crude", "lng", or "oil" (case-insensitive).
  3. AIS numeric vessel type in 80–89 (AIS spec: tankers).
  4. Fallback: vessel is inside an oil or LNG port's radius (Ras Tanura, Kharg, Jubail, Mina al-Ahmadi, Basra, Ras Laffan, Fujairah) and has no enrichment yet — most vessels physically berthed at oil terminals are oil tankers.

The stricter "Crude only" filter requires cargo_type == "crude_oil", AIS numeric type 84, or the word "crude" in the ship-type text — no port-proximity fallback.

Average wait time

Computed per vessel as hours = now − arrival_at_zone. Arrival is sourced in this order of preference:

  1. Pinned arrival from the vessel_waiting_arrival table, which stores the first-observation timestamp the moment a vessel enters its waiting zone. Not clipped to the scan window, so this is the honest figure for long queues.
  2. Crisis-onset floor. If a vessel is currently in a waiting zone and we have no pinned arrival yet (pre-deploy vessels or backfill not run), the wait is floored at the known crisis-start date configured in settings.crisis_started_at_iso — the strait has been restricted since then, so these vessels have waited at least that long.
  3. 72h scan-window estimate (earliest observation in current zone within the 7-day position window). Fallback only; clips at 7 days which heavily under-reports multi-week queues.

The Wait Times page shows aggregate averages and a per-vessel table sorted by longest wait descending. Admins can run a position-history backfill (viaPOST /api/admin/backfill-waiting-arrivals) to populate pinned arrivals from Data Docked's 2-year historical endpoint, at a cost of ~1 credit per vessel.

Ghost ships

A vessel is flagged as a "ghost" when its AIS broadcast pattern implies movement its transponder isn't accounting for. Three buckets:

  • Crossed while dark: last observed on one side of the strait, resurfaced on the opposite side. Suggests a transit with AIS disabled — the strongest signal in sanctions- evasion investigations.
  • Still dark: silent for longer than (feed_age + 8h). The feed-age floor prevents everyone from looking "dark" simply because the upstream AIS provider is lagging.
  • Resurfaced same side: long silence, returned to the same gulf. Weaker signal — legitimate reasons exist (lingering in port, turning around), but worth tracking.

Runs against a rolling 14-day position window. Purges rows older than 3 hours each run to keep the view fresh. Full list at /ghost-ships.

Port activity carry-forward

Hourly port rollups used to write zero for every port when the AIS feed was empty, creating fake "cliff" artifacts in port timeseries. The current loop detects a fully-empty feed tick and carries forward the most recent good row per port, settingcarried_forward: trueon the row so the UI can style it differently.

Strait status tiers

Computed from throughput_pct = actual_transits / baseline_daily × 100 and a crisis-oil flag (Brent > $90/bbl):

  • CLOSED— zero transits observed, or throughput < 5% while crisis oil.
  • SEVERELY RESTRICTED— throughput < 25% (or < 40% with crisis oil).
  • RESTRICTED— throughput < 75%.
  • OPEN — otherwise.

Baseline: 60 large-vessel transits per day through the TSS (UNCTAD/EIA reference). Configurable via normal_daily_transits.

War-risk premium multiplier

Computed as max(war_risk_pct) / 0.1% across the most recent quote from every insurer in the insurance_premium table. The baseline of 0.1% of hull value is the pre-June 2024 norm for VLCCs transiting Hormuz. Quotes are curated — not automated.

News relevance

RSS feeds (BBC Middle East, Al Jazeera, NYT, Reuters, CNBC) and NewsAPI queries are passed through a keyword gate: an article is kept only if its title or summary contains at least one of — Hormuz, Iran, Tehran, Khamenei, IRGC, Bandar Abbas, Kharg, Persian Gulf, Gulf of Oman, tanker, OPEC, sanctions, 5th Fleet, CENTCOM, Houthi, Red Sea, Bab el-Mandeb. Off-topic articles are not stored.

Data sources

  • Terrestrial AIS — aisstream.io (continuous WebSocket stream)
  • Satellite AIS — Data Docked (8 h cadence, 2 h during crisis)
  • Oil prices — Yahoo Finance (minute-level) and EIA (daily)
  • Strategic reserves — U.S. EIA weekly stocks
  • News — Google News, BBC, Al Jazeera, NYT, Reuters, CNBC, NewsAPI
  • Truth Social — filtered for Hormuz / Iran / oil keywords
  • Carriers, insurance, pipelines — manually curated admin entries

See the sources page for per-entity URLs and freshness timestamps.

Caching

Nginx serves cached responses with stale-while-revalidate for most endpoints: live metrics 30 s, summaries 5 min, historical series 60 min, Next.js static assets 1 day (immutable). Authenticated requests and admin writes bypass the cache. The X-Cache-Status header is exposed on every response.