Trade surveillance teams cannot wait minutes for a model to decide whether a burst of orders looks like spoofing. A practical low latency llm trade surveillance system decouples event ingestion from inference and treats the LLM as a streaming judge with hard timeouts. This post walks through building that pipeline from Kafka ingest to structured LLM scoring, with fallback and caching that survive provider outages.
Step 1: Lock the input contract and pre-filter with rules
Raw trade feeds carry thousands of events per second. Sending every fill to an LLM will blow your budget and your tail latency. Write a deterministic pre-filter that emits only candidate alerts: size outliers, rapid cancel/replace cycles, or price walks outside the NBBO.
Define a minimal alert schema:
{
"alert_id": "a1b2c3",
"trader_id": "T-882",
"instrument": "AAPL",
"signals": ["cancel_rate>0.8", "layering"],
"window_ms": 5000,
"events": [{"ts": 1710000000000, "side": "buy", "qty": 1000, "px": 192.5}]
}
A Python rule can run inside your stream processor:
def is_candidate(trade_window: dict) -> bool:
cancels = trade_window["cancels"]
orders = trade_window["orders"]
if orders and cancels / orders > 0.8:
return True
if trade_window["px_drift_bps"] > 15:
return True
return False
Push only candidates to a dedicated surveillance.llm.in topic. This cuts inference volume by 95% in most equities flows. Consume with a Kafka client that commits offsets only after the alert is persisted, so a crash replays at most one window:
from kafka import KafkaConsumer
consumer = KafkaConsumer("trades.raw", group_id="surv-pre")
for msg in consumer:
window = deserialize(msg.value)
if is_candidate(window):
producer.send("surveillance.llm.in", to_alert(window))
consumer.commit()
The pre-filter is the single highest-leverage step in any low latency llm trade surveillance design. If you skip it, no model or gateway will save you.
Step 2: Build a constrained prompt with fixed output shape
LLM latency scales with output tokens. For low latency llm trade surveillance, force the model to return a strict JSON verdict, not prose. Use a system prompt that bans explanations and a user prompt that injects the alert schema.
SYSTEM = """You are a trade surveillance classifier.
Return JSON only: {"risk":"low|med|high","reason":"<8 words>"}.
No markdown, no commentary."""
def make_user(alert: dict) -> str:
return json.dumps(alert)
Set max_tokens=32 and temperature=0. If your gateway supports provider cache-control, mark the system prompt as immutable:
headers = {"X-Cache-Control": "system:ttl=3600"}
That single header lets an OpenAI-compatible endpoint reuse the compiled prompt prefix across calls, shaving tens of milliseconds per request. Keep the user payload small; strip event arrays longer than ten entries and summarize counts instead. The model does not need every tick to flag layering.
Step 3: Stream inference and parse incrementally
Blocking on a full response doubles perceived latency. Use streaming and parse the JSON as tokens arrive. The OpenAI SDK emits deltas; accumulate and json.loads when the brace closes.
import asyncio, openai, json
client = openai.AsyncOpenAI(base_url="https://api.openai.com/v1", api_key="KEY")
async def score_alert(alert: dict) -> dict:
stream = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role":"system","content":SYSTEM},
{"role":"user","content":make_user(alert)}],
max_tokens=32, stream=True, temperature=0)
buf = ""
async for chunk in stream:
buf += chunk.choices[0].delta.content or ""
if buf.count("{") and buf.count("}") and buf.strip().endswith("}"):
try:
return json.loads(buf)
except json.JSONDecodeError:
pass
return {"risk":"med","reason":"parse-timeout"}
Wrap the call in asyncio.wait_for with a 1.5s deadline. If the stream misses the deadline, drop to a rule-based fallback score. That deadline is the backbone of low latency llm trade surveillance: the model enhances the rules but never gates the alert queue.
Use a pydantic model to validate the parsed object so a malformed high verdict never reaches the case manager:
from pydantic import BaseModel
class Verdict(BaseModel):
risk: str
reason: str
Step 4: Route through a resilient gateway
Providers throttle. When a primary model returns 429, you need instant fallback to a secondary without rewriting your client. An OpenAI-compatible endpoint that addresses 240+ models and honors client routing directives lets you specify X-Route-Order: primary,secondary and get automatic failover. n4n.ai exposes exactly that behavior, plus per-token usage metering so you can attribute cost per surveillance desk.
Configure the client once:
client = openai.AsyncOpenAI(
base_url="https://api.n4n.ai/v1",
api_key="KEY",
default_headers={"X-Route-Order":"gpt-4o-mini,mixtral-8x7b"}
)
The gateway forwards cache-control hints to the upstream, so your static system prompt stays cached even on the fallback model. This removes the operational drag of multi-provider SDKs from the hot path. You also get a single per-token bill instead of reconciling three provider invoices.
Step 5: Backpressure and batching at the edge
Surveillance bursts cluster around volatility events. Use a bounded asyncio queue between the Kafka consumer and the scorer. If the queue exceeds 100 items, shed load by skipping low-signal candidates (those with only one flag).
queue = asyncio.Queue(maxsize=100)
async def worker():
while True:
alert = await queue.get()
if queue.qsize() > 80 and len(alert["signals"]) < 2:
continue
verdict = await asyncio.wait_for(score_alert(alert), 1.5)
await emit_verdict(alert, verdict)
This keeps p99 scoring latency flat when the feed spikes 10x. Do not batch multiple alerts into one LLM call; the output schema becomes ambiguous and you lose per-alert timeout control. Parallelism comes from many workers, not from packing prompts.
Step 6: Verify success with replay testing
You cannot claim low latency llm trade surveillance works without a replay harness. Capture a day of production trade windows, inject synthetic layering patterns, and run the pipeline against a shadow topic.
Success criteria:
- End-to-end p99 from alert emit to verdict persist < 2.0s under 50 concurrent candidates.
- Model fallback triggers within one request on forced 429s.
- Cache hit rate on system prompt > 99% (check gateway metrics).
- Zero alerts lost due to timeout; all timeouts produce rule-based verdict.
Run the replay with kafka-console-producer and a small Python driver:
kafka-console-producer --topic surveillance.llm.in --bootstrap-server localhost:9092 < replay.jsonl
Then query your verdict store:
SELECT risk, count(*) FROM verdicts
WHERE ts > now() - interval '1 hour'
GROUP BY risk;
If high risk alerts correlate with injected patterns and latency SLO holds, the pipeline is production-ready. Automate this replay in CI with a fixed fixture so regressions in prompt size or timeout surface before deploy.
Step 7: Instrument tail latency, not averages
Average latency lies. A 200ms mean hides a 3s tail that misses a regulatory window. Export per-request durations from the score_alert wrapper to Prometheus:
from prometheus_client import Histogram
LAT = Histogram("llm_score_seconds","LLM surveillance scoring")
async def scored(alert):
with LAT.time():
return await score_alert(alert)
Alert on llm_score_seconds{quantile="0.99"} > 1.5. When that fires, your fallback already engaged; the alert means the secondary path is also slow and you should shed load. Build a Grafana panel that overlays queue depth with p99 to see cause and effect during volatility.
Step 8: Audit logging without PII leakage
Surveillance outputs are subject to books-and-records rules. Log the verdict and alert ID, but redact trader names at the edge if your schema carries them. Store the raw alert in an append-only bucket with restricted access.
def log_verdict(alert, verdict):
audit.info("verdict", alert_id=alert["alert_id"], risk=verdict["risk"])
Keep the LLM response reason field; regulators accept model-generated rationale if it is consistent and reproducible. Pin the model version in your gateway route so a year-old case can be re-scored identically.
Building low latency llm trade surveillance is less about model choice and more about plumbing: pre-filter hard, stream strict, fail over silently, and measure the tail. Do that and the compliance team gets answers before the trade prints clear.