You Got an ADA Demand Letter. Now What?

ADA-related demand letters targeting websites have grown roughly 14% year-over-year since 2018. If you just received one, here’s what to do — and what not to do — in the first 72 hours.

First 24 hours

  • Don’t reply directly. Forward to your lawyer (or get one) before any written communication. Anything you send becomes evidence.
  • Preserve the page state. Take a full HTML + screenshot snapshot of the URL(s) cited in the letter, today’s date. Their claims are time-bound; you need proof of what was live when.
  • Run a baseline scan. axe-core, WAVE, AccessProof — anything that produces a date-stamped issue list. This becomes the starting point of your remediation log.

Day 2–7

  • Triage findings: critical/serious first (color contrast, missing labels, keyboard traps).
  • Document each fix with a commit / ticket / dated entry. The paper trail matters more than the speed.
  • Add an accessibility statement page (publicly linked from the footer) describing your standard, conformance level, contact, and remediation plan.

What reduces risk long-term

Most plaintiffs settle out of court if you can show continuous, dated remediation activity. A single audit means nothing; quarterly automated scans + a public statement + a contact form do.

Common mistakes

  • Negotiating directly with the plaintiff before counsel.
  • Removing the offending page (looks like evidence destruction).
  • Using an “accessibility overlay” widget. Courts increasingly view them as cosmetic; they don’t fix underlying issues and may add new ones.

How AccessProof helps

Each scan produces a dated, exportable PDF report. Schedule weekly or monthly scans, archive the PDFs, and you have the contemporaneous documentation that turns “we tried” into “we proved we tried”.

Originally published on access-proof.com.

BriefOps AI

I built BriefOps AI for the #BuiltWithMeDo Hackathon.

Try out here: https://app-bh1ji3id0oap.appmedo.com/

BriefOps AI is an AI-powered incident response workspace that turns production alerts into structured incident briefs, war rooms, runbooks, stakeholder updates, GitHub issue reports, postmortems, and auditable incident artifacts.

Flow:
Alertmanager → BriefOps AI → AI Brief → War Room → Telegram/Discord/Slack/GitHub → Incident Artifacts

Built with MeDo, Supabase, Edge Functions, and AI analysis.

WCAG 2.2: Every New Success Criterion Explained

WCAG 2.2 (published October 2023) introduced 9 new success criteria. Most of them target real-world friction points — touch targets, focus visibility, drag operations — rather than purely conceptual rules. Here’s what each one means in practice.

The 9 new criteria

2.4.11 Focus Not Obscured (Minimum) — AA

When an element receives focus, it must not be entirely hidden by sticky headers, banners, or modals. Common offender: a sticky cookie banner that covers the focused link in the menu just below.

Fix: scroll-padding-top equal to the sticky header height, or move focus to a position that accounts for the overlay.

2.4.12 Focus Not Obscured (Enhanced) — AAA

Same as above, but the focused element must be fully visible (no partial occlusion).

2.4.13 Focus Appearance — AAA

The focus indicator must have a minimum area (2 CSS px around the perimeter) and a 3:1 contrast ratio against the unfocused state. Killing the default outline without replacing it fails this.

2.5.7 Dragging Movements — AA

Anything done with a drag must be doable with a single pointer (click/tap). Sliders, kanban boards, signature pads — all need a non-drag fallback.

2.5.8 Target Size (Minimum) — AA

Interactive targets must be at least 24×24 CSS px (with exceptions for inline text and spacing-based equivalents). Tiny social icons in footers are the most common failure.

3.2.6 Consistent Help — A

If a help mechanism is repeated across pages (chat widget, contact link), it must appear in the same relative location. Don’t hide the chat on some pages.

3.3.7 Redundant Entry — A

Within the same process, don’t ask for information the user has already entered. Pre-fill or offer to reuse.

3.3.8 Accessible Authentication (Minimum) — AA

Cognitive function tests (memorize, transcribe a captcha, identify objects) must have an alternative. Allow paste in password fields. Offer SSO or magic links.

3.3.9 Accessible Authentication (Enhanced) — AAA

No cognitive function test at all, even with alternatives. Object-recognition CAPTCHAs disqualify you.

How AccessProof helps

Of these, the criteria reliably catchable by automated tooling (axe-core 4.9+) are 2.5.8 (target size) and partial coverage of 2.4.11 (focus obscured). The rest require manual review or process audit. AccessProof flags the automatable ones automatically and prompts you for the manual checks at the end of each scan.

Originally published on access-proof.com.

I built a build tool in Go — and now I’m slowly rewriting pieces in Rust (fz 1.9.0 release)

So I’ve been solo-building fz — a fast, opinionated build tool for C/ASM projects written in Go. No Makefile hell, no CMake nightmares. Just a single binary that figures out your toolchain and gets out of the way.

Today I shipped 1.9.0 (and a quick 1.9.2 patch). Here’s what changed — and why I started poking at Rust.

What’s new in 1.9.0

Cross-compilation with -target <triple>

fz -target arm-linux-gnueabihf
fz -target riscv64-linux-gnu
fz -target x86_64-w64-mingw32

fz now auto-selects the right compiler, assembler, and linker for the target triple. ARM, RISC-V, x86_64, i386 — all supported out of the box.

Static libraries: -type static / -lib

fz -type static -lib mylib

Builds .a archives from object files via ar. Previously you had to wire this up yourself. Now it’s a one-liner.

LSP support: compile_commands.json

fz -compile-commands

Generates compile_commands.json for clangd and any other LSP client. Finally get autocomplete and go-to-definition in your editor without configuring anything manually.

Other improvements

  • fz -shell now handles both single files and full directories
  • Fixed a long-standing bug where hello.asm and hello.s would produce conflicting object file names
  • Linker test coverage: 17% → 60% 🎉
  • Squashed dozens of golangci-lint warnings

1.9.2 patch — the boring-but-important stuff

Cleaned up every errcheck, govet, and ineffassign warning across the codebase:

  • Proper error handling for json.Encode, os.Rename, os.Chmod, storeCache
  • Error checks in tests: os.Chdir, os.MkdirAll, os.Create, buf.ReadFrom
  • Fixed an empty branch in builder_test.go and a wrong printf format
  • Updated go.mod / go.sum

Nothing glamorous, but the kind of hygiene that prevents weird bugs six months from now.

The Rust experiment: rustcache

⚠️ This lives in a separate branch and is not part of the release. Yet.

I’ve been curious about Rust FFI for a while, so I decided to try moving the caching layer — file hashing and copying — into a Rust static library.

Here’s what I’ve built so far in rustcache/:

// SHA256 of a file, returned as a hex string over FFI
#[no_mangle]
pub extern "C" fn rust_hash_file(path: *const c_char) -> *mut c_char { ... }

// Simple file copy via std::fs::copy
#[no_mangle]
pub extern "C" fn rust_copy_file(src: *const c_char, dst: *const c_char) -> i32 { ... }

5 unit tests, all passing. Builds into librustcache.a.

The plan

Once I wire it into Go via cgo and build tags:

//go:build rustcache

// #cgo LDFLAGS: -L./rustcache/target/release -lrustcache
// #include "rustcache.h"
import "C"

…users will be able to opt in with -tags rustcache and I’ll benchmark both paths. If the Rust version is meaningfully faster, it becomes the default.

For now: zero impact on existing fz users. The Go cache path is unchanged.

What’s next

  • [ ] Finish the Rust → Go integration (cgo, build tags, benchmarks)
  • [ ] Dynamic library support (.so / .dylib)
  • [ ] Colored error and warning output
  • [ ] Remote cache (S3 / MinIO) for CI/CD speedups

If you’re building C or ASM projects and tired of Makefile archaeology, give fz a try. Issues and PRs welcome — it’s a solo project and I read everything.

Docs and install: https://github.com/forgezero-cli/forgezero

A Scaling Lesson Building Production-Grade Agentic AI Systems

A Scaling Lesson Building Production-Grade Agentic AI Systems

One of the early observations we had while designing enterprise AI agents was this:

Giving an agent more tools does not necessarily make it smarter.

In theory, it sounded correct.

If an agent had access to customer systems, payment systems, inventory, shipping, reporting, ticketing, email, scheduling, analytics, and internal knowledge bases — it should become more powerful and autonomous.

But what we observed in real implementations was very different.

The more tools we added, the more unstable the system became.

Not because the model was weak.

Not because the tools were poorly built.

But because the agent’s decision space became too large.

For every user request, the agent had to evaluate all available tools, compare descriptions, infer intent, decide sequencing, and determine the best execution path.

Now imagine doing this with 18 tools:

  • Customer lookup
  • Order search
  • Refund processing
  • Inventory checking
  • Shipping tracking
  • Email sending
  • Ticket creation
  • Knowledge base search
  • Sentiment analysis
  • Language translation
  • Calendar scheduling
  • Report generation
  • Data export
  • User authentication
  • Payment processing
  • Discount application
  • Feedback collection
  • Escalation routing

Initially, everything looked manageable.

But as workflows became more dynamic, we started observing:

  • wrong tool selection,
  • unnecessary tool chaining,
  • higher latency,
  • increased token usage,
  • inconsistent execution paths,
  • and occasional hallucinated actions.

The problem was not intelligence.

The problem was cognitive overload inside the orchestration layer.

Over time, one pattern became very clear:

Agents perform significantly better when their responsibility boundaries are smaller.

In our experience, once an agent moves beyond roughly 4–5 actively usable tools, reliability starts dropping rapidly. Similar enterprise orchestration patterns are now recommending smaller, specialized agents instead of monolithic “super agents.”

That observation changed how we started designing AI systems.

Instead of building one massive “do everything” agent, we moved toward specialized agents with tightly scoped responsibilities.

For example:

A support agent handles:

  • customer lookup,
  • ticket creation,
  • escalation routing,
  • knowledge retrieval.

A commerce agent handles:

  • orders,
  • refunds,
  • discounts,
  • payments.

An operations agent handles:

  • shipping,
  • inventory,
  • reporting,
  • exports.

This immediately improved:

  • tool accuracy,
  • execution consistency,
  • observability,
  • debugging,
  • latency,
  • and operational trust.

But another important learning came later.

Even after distributing tools properly, systems still degraded when too many agents were active simultaneously.

This is something many teams underestimate.

As the number of agents increases, coordination overhead also increases:

  • more inter-agent communication,
  • more memory synchronization,
  • more orchestration reasoning,
  • more retries,
  • more conflict resolution,
  • and more state tracking.

At lower scale, this is manageable.

At enterprise scale, it becomes a serious engineering challenge.

We observed cases where:

  • agents started waiting on each other,
  • orchestration layers became bottlenecks,
  • duplicate reasoning increased token burn,
  • cascading retries created operational instability,
  • and observability became extremely difficult.

Multi-agent systems introduce their own scaling complexity around coordination, governance, and orchestration overhead. Most production-grade architecture guidance today recommends keeping orchestration layers as simple as possible.

Over time, we established a few practical thumb rules internally.

Some Practical Thumb Rules We Follow Now

1. Keep Tool Count Small Per Agent

Our practical guideline today is:

  • 3–5 tools → ideal
  • 6–8 tools → manageable with careful prompting
  • 10+ tools → requires routing/filtering layers
  • 15+ tools → usually an architectural warning sign

The issue is not model capability.

It is decision dilution.

2. Every Agent Must Have One Clear Business Responsibility

We avoid mixing domains.

For example:

  • payments + support,
  • analytics + execution,
  • reporting + approvals,
  • inventory + customer engagement.

The narrower the responsibility boundary, the more predictable the behavior.

3. Start With the Lowest Complexity Possible

One important learning from enterprise orchestration patterns is this:

Do not introduce multi-agent architecture unless the workflow genuinely requires it.

Sometimes:

  • a prompt is enough,
  • sometimes a single agent is enough,
  • sometimes workflows are better handled through deterministic orchestration.

Not every problem needs “AI teamwork.”

4. Avoid Excessive Agent-to-Agent Conversations

Agent collaboration sounds powerful in demos.

But in production:

  • every interaction increases latency,
  • every message consumes tokens,
  • every dependency creates failure paths.

We now aggressively reduce unnecessary conversations between agents.

5. Retrieval Before Reasoning

Instead of exposing all tools to all agents, we first narrow candidates through:

  • semantic routing,
  • metadata filtering,
  • RAG-based retrieval,
  • workflow classification.

This significantly improves tool selection accuracy and reduces reasoning load.

6. Observability Is Mandatory

Once systems become multi-agent, debugging becomes one of the hardest engineering problems.

We now treat the following as first-class requirements:

  • distributed tracing,
  • token tracking,
  • step-level logging,
  • execution replay,
  • agent health monitoring,
  • retry visibility,
  • and orchestration graphs.

Without observability, production support becomes nearly impossible.

7. Human Escalation Is Still Critical

One thing we intentionally avoid is trying to automate every decision.

We now introduce human checkpoints for:

  • financial operations,
  • policy-sensitive actions,
  • low-confidence reasoning,
  • and customer-impacting workflows.

Autonomy without governance becomes operational risk.

What I increasingly believe is that the future of enterprise AI is not one giant super-agent.

It is orchestrated systems of smaller specialized agents collaborating through routing, delegation, memory sharing, and controlled execution.

The real engineering challenge is no longer:
“How many tools can an agent use?”

The better question is:
“How effectively can we reduce the decision burden for each agent while keeping orchestration manageable?”

That has become one of the most important scaling lessons for us while building production-grade agentic AI systems.

How We Are Thinking About This in Cloud Architecture

One important realization for us was that multi-agent systems should not be treated as a single application deployment.

They should be treated as distributed cloud-native systems.

That changes the architecture significantly.

Today, the architecture pattern we increasingly follow looks something like this:

Specialized Agents as Independent Services

Each agent runs independently with:

  • isolated APIs,
  • dedicated scaling,
  • separate observability,
  • isolated memory/context,
  • and domain-level permissions.

This reduces blast radius and improves operational governance.

In AWS, this naturally aligns very well with:

  • Lambda,
  • ECS/EKS,
  • event-driven services,
  • queues,
  • Bedrock,
  • and serverless orchestration patterns.

What I personally liked while evaluating newer AWS patterns is how Amazon Bedrock AgentCore is trying to standardize several production concerns around agents. Instead of teams writing custom orchestration glue repeatedly, AgentCore is introducing managed capabilities around:

  • runtime isolation,
  • observability,
  • memory,
  • identity,
  • tool gateways,
  • and orchestration patterns.

One thing I strongly relate to from practical experience is this:

Building the reasoning layer is usually not the hardest part anymore.

The harder part is:

  • orchestration,
  • debugging,
  • tracing,
  • retries,
  • governance,
  • and operational scalability.

That is where systems usually become unstable at scale.

AWS AgentCore Observability is also moving in an interesting direction by treating agent execution visibility as a first-class production capability with:

  • execution tracing,
  • token monitoring,
  • latency tracking,
  • tool usage visibility,
  • and CloudWatch integration. ()

When you have multiple agents collaborating dynamically, you need visibility into:

  • why a tool was selected,
  • which agent delegated the task,
  • what context was shared,
  • where retries happened,
  • and why execution paths changed.

Without this, production debugging becomes extremely difficult.

Another pattern we increasingly prefer is asynchronous orchestration.

Instead of tightly coupling agents synchronously, we now lean more toward:

  • queues,
  • events,
  • workflow engines,
  • and loosely coupled communication.

It improves:

  • resilience,
  • scalability,
  • retry handling,
  • and fault isolation.

Most importantly, it prevents one overloaded agent from slowing down the entire system.

What I increasingly believe is that the future of enterprise AI is not one giant super-agent.

Breaking Logging’s Flywheel of Compromises

The 3C flywheel of compromises

Authored by Mike Neville-O’Neill

Let’s face it — logging is broken. Not just a little broken, but fundamentally misaligned with the needs of modern engineering teams. At a recent AWS Summit talk in London, Benoit Gaudin (our Head of Infrastructure) and I shared Bronto’s vision for fixing this mess once and for all.

The Problem We’re All Living In

The 3C flywheel of compromises

If you’re running any significant infrastructure today, you’re probably stuck in what we call the “3C flywheel of compromises”:

  • Cost — Logging at scale has become ridiculously expensive
  • Coverage — So you cut corners, dropping those infra logs and long-tail workflows
  • Complexity — And end up with a Frankenstein’s monster of 5–8 different systems duct-taped together

This isn’t just inefficient — it’s actively harmful. Engineers end up building parallel solutions just to get basic visibility because the main tool is too limited, too slow, or too expensive.

Logs Matter More Than Ever

Logs aren’t just a compliance checkbox anymore. They’re your operational ground truth in the AI era.

They feed your LLMs. They power your agents. They’re your audit trail, your RAG source, your behavioral training set. And one log message from an LLM-based system might contain 50–100 nested events in a single payload.

Try scaling that with a solution built before the separation of compute and storage was even a thing.

How We’re Breaking the Cycle

Bronto was built to tackle this head-on with three non-negotiable capabilities:

  1. Subsecond search on all logs — whether they’re two seconds or two years old
  2. Petabyte-scale retention — no infrastructure for you to manage
  3. Completely different pricing — think cents per GB, not dollars

The platform is built natively on AWS (S3, Lambda, DynamoDB), but engineered so you don’t have to deal with pipelines, pre-processing, or glue code.

Bronto’s Architectural Advantage

Bronto architecture diagram

The ingestion layer accepts data from standard sources — OpenTelemetry Collector, FluentD, FluentBit — through HTTP endpoints, with AWS EC2 load balancers doing the heavy lifting. Data is buffered through Kafka (AWS MSK), but then things diverge from the standard playbook.

Instead of traditional approaches, data is processed from Kafka and written to S3 in a proprietary format that borrows techniques from data analytics: data partitioning, Bloom filtering, push predicates, compression, and columnar-based formats. Metadata lives in DynamoDB for speed.

The real magic happens at search time. When you query through the UI or API, Lambda functions launch in parallel and process data directly from S3. No overprovisioning for big queries — horizontal scaling on demand, paying only while functions run.

This architecture is what enables both the performance (subsecond on terabytes, seconds on petabytes) and the pricing model. No expensive clusters running 24/7 — just cloud resources used exactly when and where they’re needed.

Real Teams, Real Results

API-First Content Platform

A team running a massive content delivery platform, serving APIs behind a global CDN for websites, mobile apps, and e-commerce systems. Every request hits their API with a unique key — they need to trace errors, group by status codes, and export logs to their own customers.

Before Bronto

  • 40TB monthly ingestion cap
  • 30+ minute query times (when they worked at all)
  • Dashboards that routinely failed
  • Constant budget pressure

After Bronto

  • Boosted ingestion to 60TB monthly
  • Cut their logging bill in half
  • Complex multi-day queries now return in subseconds
  • Built reliable log exports for their own customers

Their exact words? “Bronto changed our lives.” A logging tool. Actually improving engineers’ lives.

Global SaaS Project Management Platform

A company running a suite of SaaS tools across distributed cloud services and product lines.

Before Bronto

  • Graylog for live logs
  • S3 for long-term storage
  • HAProxy logs dumped into S3 with gnarly Athena queries
  • A mix of Athena, Superset, and QuickSight for analytics
  • Just 1–2 days of retention across most systems

After Bronto

  • Everything centralized — HAProxy, Kubernetes, application logs, audit trails
  • Extended to 90-day hot retention
  • Real dashboards tracking error spikes, traffic anomalies, and app version drift
  • Engineers focused on product, not maintaining logging infrastructure

They went from managing logs to actually using them.

Logs as Your Secret Weapon

Your log data is massively undervalued — not because it lacks signal, but because current tooling hides that signal behind cost barriers, friction, and compromises.

Logs used to be a liability. With the right approach, they can be your secret weapon.

We’re building Bronto to be for logging what Dyson was for vacuum cleaners, what iPhone was for smartphones, and what Tesla was for electric cars — a complete reimagining of what’s possible when you refuse to accept the status quo.

After all, when was the last time your logging tool made your life better instead of worse?

See Bronto in Action

The AI Failure Mode That Costs Professionals the Most (And How to Detect It)

Knowledge workers spend an average of 4.3 hours per week fact-checking AI outputs. Most of that time is wasted on the wrong failure mode.

Most people worry about AI hallucinations,that is, when AI fabricates false information. But that’s not the most dangerous failure mode.

The real risk lies in what I call plausible-neighbor substitution: the AI provides an answer that is statistically close to the correct one but ultimately incorrect. Instead of inventing entirely new content, it offers:

  • The dosage for a related medication

  • The regulatory threshold from last year

  • A clause from an adjacent jurisdiction

  • Code that works on test cases but fails at scale

Why is this more problematic than hallucinations? Because it often passes casual inspection. A hallucinated citation collapses immediately once checked. In contrast, a plausible neighbor appears legitimate and survives superficial review, it’s crafted to look exactly like what the correct answer should be.

The solution is simple: ask a targeted question ->
“What is the most common incorrect answer to this question, and how does your answer differ from it specifically?”

This prompts the AI to distinguish its response from its most probable error. It succeeds roughly 70% of the time; in the remaining 30%, manual verification is necessary.

I’ve documented this technique along with four other failure modes in a comprehensive protocol covering 25 distinct failure types across law, medicine, coding, and strategy.

The Cross-Examination Method

ASN Lookup for Security Engineers: From Concept to Code

An ASN lookup is the fastest way to find out who actually operates the network behind an IP address. Every publicly routable IP on the internet belongs to an autonomous system, and that autonomous system number (ASN) identifies the network operator, what infrastructure they run, and who they peer with. For security engineers, this is a first-order pivot point: when an IP shows up in your logs tied to credential stuffing or port scanning, the ASN tells you whether you’re looking at a residential ISP, a cloud hosting provider, or a known bulletproof hosting operation.

This tutorial covers what ASNs are, how they fit into internet routing, and then gets to the practical part: querying ASN data programmatically with curl, Python, and JavaScript so you can fold it into your investigation workflow or SIEM pipeline.

TL;DR

  • An ASN (Autonomous System Number) uniquely identifies a network that participates in internet routing via BGP.
  • As of 2025, roughly 120,000 ASNs had been allocated globally, while the number actively visible in BGP routing tables varied by dataset and vantage point.
  • ASNs classify into three types: stub (one upstream), multi-homed (multiple upstreams), and transit (carries traffic for others).
  • Security engineers use ASN data for threat attribution, network-level blocking, infrastructure mapping, and log enrichment.
  • Programmatic ASN lookups return the network operator, organization type (ISP, hosting, business, education), route prefixes, and peering relationships.
  • This tutorial includes working code for ASN lookups in curl, Python, and JavaScript using ipgeolocation.io’s ASN API.

In short: IP addresses identify devices, ASNs identify the networks those devices belong to. Knowing the network behind an IP is often more actionable than knowing the IP itself.

What Is an Autonomous System (and Why Does It Have a Number)

An autonomous system (AS) is a collection of IP address ranges, called prefixes, that operate under a single routing policy managed by one organization. An ISP like Comcast, a cloud provider like AWS, and a CDN like Cloudflare each operate their own autonomous systems. Internally, an AS can run whatever routing protocols it wants (OSPF, IS-IS, EIGRP). Externally, it presents one consistent set of rules about which IP prefixes it controls and how traffic reaches them.

Each AS gets a unique number: its ASN. These are assigned by IANA through five Regional Internet Registries (RIRs): ARIN (North America), RIPE NCC (Europe, Middle East, Central Asia), APNIC (Asia-Pacific), LACNIC (Latin America), and AFRINIC (Africa). The original 16-bit format allowed 65,536 ASNs. The 32-bit format, standardized in RFC 6793, extends that to over 4.2 billion. As of 2025, roughly 120,000 ASNs had been allocated globally, while the number actively visible in BGP routing tables varied by dataset and vantage point.

Three types matter in practice:

  • Stub AS: connects to only one upstream provider. A small company with a single ISP connection. Most ASNs are stubs.
  • Multi-homed AS: connects to two or more upstreams for redundancy. A mid-size enterprise with connections to both Cogent and Lumen.
  • Transit AS: carries traffic between other autonomous systems. The backbone providers (AS3356 Lumen, AS2914 NTT, AS174 Cogent) that form the internet’s core routing infrastructure.
    The glue that holds all of this together is BGP (Border Gateway Protocol). When an AS wants the internet to know about its IP prefixes, it announces them via BGP. Each announcement includes an AS-PATH: the sequence of ASNs that traffic traverses to reach the prefix. Routers use the AS-PATH to choose the best route, typically the shortest one.

A concrete example: your browser requests a page hosted in Frankfurt. Your ISP (say, AS7922 Comcast) checks its BGP table and finds that the shortest path to the destination prefix goes through AS3356 (Lumen) to AS24940 (Hetzner). Three ASNs, three AS-path hops.

Why Security Engineers Care About ASN Data

Threat Attribution Beyond the IP

An IP address can change hands hourly in cloud environments. An ASN identifies the network operator, which is far more stable. When you see a cluster of malicious requests coming from 5 different IPs, they might all resolve to the same ASN. That’s not 5 separate actors; that’s one network.

Suppose your WAF flags credential stuffing attempts from 185.220.101.x. You look up the ASN: AS205100, F3 Netze e.V., a Tor exit node operator in Germany. That single fact reframes the incident. You’re not dealing with a compromised residential user; you’re dealing with traffic deliberately routed through an anonymization network. The response changes from “investigate the user” to “rate-limit the network.”

Tip: The type field in ASN data is your first filter. HOSTING and ISP ASNs produce most bot traffic. EDUCATION and BUSINESS ASNs are more likely to be legitimate. It’s a rough heuristic, not a rule, but it cuts noise.

ASN-Based Blocking and Allowlisting

Individual IP blocking is a game of whack-a-mole. Attackers rotate IPs constantly. ASN-level blocking is coarser but more durable: if an autonomous system consistently sources malicious traffic, blocking it at the network level eliminates entire IP ranges at once.

A practical example: hosting providers like Hetzner (AS24940), OVHcloud (AS16276), and DigitalOcean (AS14061) are popular with both legitimate developers and attackers. You probably can’t block them outright. But you can apply stricter rate limits and CAPTCHA challenges to traffic originating from hosting ASNs while letting residential ISP ASNs through with less friction. The ASN’s type field (ISP vs HOSTING vs BUSINESS) enables this segmentation without maintaining manual IP lists.

Infrastructure Mapping with Network Topology

Basic ASN lookups tell you who operates a network. Topology data, upstream and downstream relationships, tells you how that network connects to the rest of the internet.

When investigating a phishing campaign, you find the domain resolves to an IP in AS64500 (a small hosting provider). Pulling the downstream data reveals that AS64500 has three downstream ASNs, two of which have shown up in threat feeds before. That’s not a coincidence; it’s infrastructure reuse across campaigns. The upstream data shows AS64500 buys transit from a single provider, which gives you an escalation path for abuse reports.

This kind of analysis is impossible with IP-level data alone. It requires the network topology layer that ASN data provides.

SIEM and Log Enrichment

Raw firewall logs are walls of IP addresses. Enriching each IP with its ASN, organization name, and network type transforms those logs from “1,000 IPs hit the login endpoint” into “380 of those IPs belong to three hosting providers, 50 belong to Tor exit ASNs, and the rest are residential ISPs.” That’s actionable intelligence.

Adding ASN data to your SIEM pipeline lets you aggregate by network operator instead of individual IP. When AS16276 (OVHcloud) shows up in 200 failed login events across 40 different IPs, that’s one signal, not 40.

How to Query ASN Data Programmatically

What You Need

  • An API key from ipgeolocation.io
  • curl (for quick lookups), Python 3.8+, or Node.js 18+
    Alternatives exist in this space. Team Cymru, HackerTarget, ipinfo, and MaxMind GeoLite2 all offer ASN data. This tutorial uses ipgeolocation.io’s ASN endpoint because it returns ASN details plus routes, peers, upstreams, and downstreams in a single call, which is the part that matters for security analysis.

Pitfall: ASN data from the unified IP geolocation endpoint on most providers gives you the basic AS number and organization. If you need route prefixes and peering relationships, you need a dedicated ASN endpoint or a separate BGP data source.

curl

Set your API key as an environment variable first:

export IPGEO_API_KEY="your_api_key_here"

The examples below pipe output to jq for readability. Drop | jq . if you don’t have it installed.

The fastest way to look up an IP’s ASN:

curl -s "https://api.ipgeolocation.io/v3/asn?apiKey=${IPGEO_API_KEY}&ip=8.8.8.8" | jq .

Response:

{
  "ip": "8.8.8.8",
  "asn": {
    "as_number": "AS15169",
    "organization": "Google LLC",
    "country": "US",
    "type": "BUSINESS",
    "domain": "google.com",
    "date_allocated": "2000-03-10",
    "asn_name": "GOOGLE",
    "allocation_status": "ASSIGNED",
    "num_of_ipv4_routes": "1737",
    "num_of_ipv6_routes": "727",
    "rir": "ARIN"
  }
}

Route counts and peering relationships change over time as BGP data evolves. The values above reflect a point-in-time snapshot.

To pull network topology for a specific ASN:

curl -s "https://api.ipgeolocation.io/v3/asn?apiKey=${IPGEO_API_KEY}&asn=AS24940&include=peers,upstreams,downstreams,routes" | jq .

This returns the full picture: every IP prefix the AS announces, who it peers with, who provides its upstream transit, and who sits downstream. For AS24940 (Hetzner), the response includes announced IPv4 and IPv6 routes, plus the transit and peering relationships that connect Hetzner to the rest of the internet.

Python

Install the requests library if you haven’t already:

pip install requests
import os
import requests

API_KEY = os.environ.get("IPGEO_API_KEY")
if not API_KEY:
    raise RuntimeError("Missing IPGEO_API_KEY environment variable")

BASE_URL = "https://api.ipgeolocation.io/v3/asn"

def lookup_ip_asn(ip_address):
    """Resolve an IP to its ASN and network details."""
    try:
        resp = requests.get(
            BASE_URL,
            params={"apiKey": API_KEY, "ip": ip_address},
            timeout=(2.0, 5.0),
        )
        resp.raise_for_status()
        data = resp.json()

        asn_data = data.get("asn", {})
        return {
            "ip": data.get("ip"),
            "as_number": asn_data.get("as_number"),
            "organization": asn_data.get("organization"),
            "type": asn_data.get("type"),
            "country": asn_data.get("country"),
            "rir": asn_data.get("rir"),
        }
    except requests.exceptions.Timeout:
        print(f"Timeout looking up ASN for {ip_address}")
        return None
    except requests.exceptions.RequestException as e:
        print(f"ASN lookup failed for {ip_address}: {e}")
        return None


def get_asn_topology(as_number):
    """Pull full network topology for an ASN: routes, peers, upstreams, downstreams."""
    try:
        resp = requests.get(
            BASE_URL,
            params={
                "apiKey": API_KEY,
                "asn": as_number,
                "include": "routes,peers,upstreams,downstreams",
            },
            timeout=(2.0, 10.0),
        )
        resp.raise_for_status()
        return resp.json().get("asn", {})
    except requests.exceptions.RequestException as e:
        print(f"Topology lookup failed for {as_number}: {e}")
        return None


if __name__ == "__main__":
    # Basic IP-to-ASN lookup
    result = lookup_ip_asn("8.8.8.8")
    if result:
        print(f"{result['ip']} -> {result['as_number']} ({result['organization']}, {result['type']})")

    # Full topology for a hosting provider
    topology = get_asn_topology("AS24940")
    if topology:
        routes = topology.get("routes", [])
        peers = topology.get("peers", [])
        upstreams = topology.get("upstreams", [])
        downstreams = topology.get("downstreams", [])
        print(f"AS24940 announces {len(routes)} routes, peers with {len(peers)} ASNs")
        print(f"  Upstreams: {[u['as_number'] for u in upstreams]}")
        print(f"  Downstreams: {[d['as_number'] for d in downstreams]}")

The two functions cover the two core use cases: lookup_ip_asn for enriching individual IPs in a log pipeline, and get_asn_topology for deeper investigation when you need to understand a network’s structure.

A longer timeout on the topology call (10 seconds vs 5) accounts for the larger response payload when routes and peering data are included.

JavaScript (Node.js)

const API_KEY = process.env.IPGEO_API_KEY;
if (!API_KEY) {
  throw new Error("Missing IPGEO_API_KEY environment variable");
}

const BASE_URL = "https://api.ipgeolocation.io/v3/asn";

async function lookupIpAsn(ipAddress) {
  const url = `${BASE_URL}?apiKey=${API_KEY}&ip=${encodeURIComponent(ipAddress)}`;

  try {
    const resp = await fetch(url, { signal: AbortSignal.timeout(5000) });

    if (!resp.ok) {
      console.error(`ASN lookup returned ${resp.status} for ${ipAddress}`);
      return null;
    }

    const data = await resp.json();
    const asn = data.asn ?? {};

    return {
      ip: data.ip,
      asNumber: asn.as_number,
      organization: asn.organization,
      type: asn.type,
      country: asn.country,
      rir: asn.rir,
    };
  } catch (err) {
    if (err.name === "TimeoutError") {
      console.error(`Timeout looking up ASN for ${ipAddress}`);
    } else {
      console.error(`ASN lookup failed for ${ipAddress}:`, err.message);
    }
    return null;
  }
}

async function getAsnTopology(asNumber) {
  const url = `${BASE_URL}?apiKey=${API_KEY}&asn=${encodeURIComponent(asNumber)}&include=routes,peers,upstreams,downstreams`;

  try {
    const resp = await fetch(url, { signal: AbortSignal.timeout(10000) });

    if (!resp.ok) {
      console.error(`Topology lookup returned ${resp.status} for ${asNumber}`);
      return null;
    }

    const data = await resp.json();
    return data.asn ?? {};
  } catch (err) {
    console.error(`Topology lookup failed for ${asNumber}:`, err.message);
    return null;
  }
}

// Usage
const result = await lookupIpAsn("8.8.8.8");
if (result) {
  console.log(`${result.ip} -> ${result.asNumber} (${result.organization}, ${result.type})`);
}

const topo = await getAsnTopology("AS24940");
if (topo) {
  const routes = topo.routes ?? [];
  const peers = topo.peers ?? [];
  console.log(`AS24940 announces ${routes.length} routes, peers with ${peers.length} ASNs`);
}

Both functions mirror the Python version. AbortSignal.timeout(5000) prevents hung connections from blocking your pipeline. The ?? (nullish coalescing) operator handles missing fields without throwing.

Reading the Response: What Each Field Tells You

The basic ASN response contains eleven fields. Here’s what matters for security work:

as_number: The ASN itself, formatted with the “AS” prefix (e.g., “AS15169”). This is the identifier you’ll use for blocking rules, SIEM correlation, and threat feed matching.

organization: The entity registered with the RIR as the AS holder. For cloud providers, this is the provider, not the customer. AS16509 is “Amazon.com, Inc.” even though the IP you’re investigating belongs to a fintech startup running on AWS.

type: One of ISP, HOSTING, BUSINESS, or EDUCATION. This is your first-pass risk signal. HOSTING ASNs produce a disproportionate share of automated attacks. ISP ASNs carry mostly residential traffic. EDUCATION ASNs are universities.

country: ISO 3166-1 alpha-2 country code for the AS registration. Not necessarily where the traffic originates (a Hetzner server in Finland still shows "country": "DE" because Hetzner is registered in Germany).

rir: Which Regional Internet Registry manages the ASN. Useful for abuse reporting: RIPE NCC has different abuse processes than ARIN.

routes (with include=routes): The IP prefixes this AS announces to the internet. This is the AS’s footprint. If you see malicious traffic from 5 IPs and all fall within prefixes announced by the same ASN, that confirms you’re dealing with one network.

peers, upstreams, downstreams (with include=peers,upstreams,downstreams): The network’s BGP relationships. Upstreams are transit providers (who the AS pays for internet access). Downstreams are customer networks. Peers are networks exchanging traffic directly. For investigation, downstreams are particularly interesting: they reveal smaller networks hiding behind a hosting provider’s ASN.

Putting It Together: An Investigation Walkthrough

You pull your morning alerts and find 47 failed SSH login attempts across 3 IPs overnight: 185.220.101.34, 185.220.101.42, and 185.220.101.52. Different IPs, same /24 range, but you want to confirm this is one actor.

Step 1: Look up the ASN for any of the three IPs. They all resolve to AS205100, F3 Netze e.V., registered in Germany. The organization name and the related prefix data point to Tor-exit traffic. That reframes the incident immediately: you’re not dealing with a compromised residential user, you’re dealing with traffic deliberately routed through an anonymization network.

Step 2: Pull the topology with include=routes,upstreams. The routes show a small number of prefixes (the 185.220.101.0/24 range among them). The upstreams show two transit providers. A stub-like topology for what is effectively a single-purpose network.

Step 3: Check your SIEM for historical traffic from AS205100. You find it appears in 12 previous incidents over the past 90 days, all brute-force attempts. That’s a pattern.

Step 4: Decision. Hard-blocking AS205100 is reasonable here because it’s a single-purpose Tor exit network, and your service has no legitimate users routing through Tor exits. For a multi-purpose hosting ASN like Hetzner or DigitalOcean, you’d rate-limit instead of block.

The whole investigation took 4 API calls and 3 minutes. Without ASN data, you’d have looked up 3 IPs individually, found they’re “in Germany,” and missed the network-level pattern entirely.

Caching and Performance Notes

ASN assignments change infrequently. An IP’s ASN changes when the IP block is sold or reallocated, which happens on the order of months or years, not hours. A 24-hour TTL on cached ASN lookups is aggressive enough for most security use cases without wasting quota on redundant calls.

For high-volume enrichment (processing millions of log entries), consider a local database instead of per-request API calls. MMDB-format ASN databases eliminate network latency entirely and support the same lookup patterns. The trade-off is update frequency: you’re working with a daily snapshot instead of real-time data. For most enrichment pipelines, that’s acceptable.

When the ASN API is unreachable, fail open. ASN enrichment adds context but isn’t load-bearing for most security decisions. Log the unenriched event and backfill later rather than dropping traffic or blocking alerts because the lookup timed out.

A Few Extra Notes

Private ASNs exist. The ranges 64512-65534 (16-bit) and 4200000000-4294967294 (32-bit) are reserved for private use. These are used internally by organizations with complex routing but should never appear in public BGP tables. If you see a private ASN in external traffic, something is misconfigured upstream, and that’s worth investigating on its own.

ASN data is one signal, not a verdict. AS16509 (Amazon AWS) hosts both legitimate SaaS products and attack infrastructure. Blocking it would break half the internet. Use ASN data to inform decisions (add friction, increase logging, require CAPTCHA), not to make them unilaterally.

The organization field can mislead. Cloud providers sublease IP ranges. The ASN registrant is Amazon, Google, or Microsoft, but the actual user of the IP is whoever rented that instance. For cloud ASNs, pair ASN data with reverse DNS, TLS certificate inspection, or company-level enrichment to get closer to the real operator.

For a deeper reference on autonomous systems, BGP routing, and ASN assignment history, the ARIN ASN guide and the ipgeolocation.io ASN guide both cover the conceptual side in more depth than this tutorial has room for.

Building an Insider Threat Detection System That Remembers Behavior Instead of Just Logging It

Most security dashboards are very good at storing events and surprisingly bad at understanding them.

That was the problem I kept running into while building InsiderShield. Existing monitoring systems could tell me that an employee downloaded files at 2:13 AM from an unfamiliar device. What they could not reliably tell me was whether that behavior actually mattered.

That distinction sounds small until you try to build a system that reduces false positives without ignoring real insider threats.

I ended up building InsiderShield around a simple idea: behavioral memory matters more than isolated events.

Instead of treating every activity log as a disconnected incident, I wanted a system that continuously learns employee baselines over time, stores evolving contextual memory, and makes decisions using historical behavioral patterns instead of static rules.

That design decision pushed me toward combining persistent memory concepts from Hindsight persistent agent memory, contextual behavioral intelligence, and runtime orchestration patterns inspired by cascadeflow runtime orchestration.

The result became a full-stack insider threat monitoring platform that continuously tracks trust evolution, behavioral deviations, and adaptive security responses in real time.

What the System Actually Does

At a high level, InsiderShield monitors employee activity and builds a continuously evolving behavioral profile for every user.

The system tracks signals like:

  • Login hour patterns
  • Trusted devices
  • Download frequency
  • File access behavior
  • Session duration
  • Location changes
  • Historical trust score evolution

Those signals are compared against a behavioral baseline stored per employee. Every new activity updates the employee’s contextual history.

The stack itself is fairly straightforward:

Layer Technology
Frontend Next.js
Backend APIs Next.js API Routes
Database MongoDB Atlas
Styling Tailwind CSS
Visualization Recharts
Behavioral Persistence Hindsight-inspired memory model
Workflow Orchestration cascadeflow-inspired execution patterns

The frontend exposes four main operational surfaces:

  1. Dashboard overview
  2. Threat Center
  3. Employee monitoring pages
  4. Activity Logs

The interesting engineering work lives underneath those screens.

The Core Problem: Security Systems Forget Everything

Most traditional monitoring pipelines behave like stateless event processors.

An employee logs in.
An event gets generated.
A rule engine checks it.
An alert maybe fires.
Then the system moves on.

That architecture breaks down quickly in insider threat detection because context matters more than isolated activity.

A midnight login is not inherently suspicious.

A midnight login from an unfamiliar device combined with abnormal download volume, unusual session duration, and foreign IP activity probably is.

The difficult part is that those signals only become meaningful when the system remembers historical behavior.

That pushed me toward designing the monitoring engine more like a persistent behavioral memory system than a conventional SIEM event stream.

I spent a lot of time studying how persistent AI memory systems using Hindsight approach contextual retention.

The key insight I borrowed was simple:

Memory should evolve continuously and influence future reasoning.

Instead of storing isolated logs, the system stores evolving behavioral baselines per employee.

Here is the simplified employee baseline structure:

baseline: {
  normalLoginHourRange: String,
  trustedDevices: [String],
  usualIPs: [String],
  normalLocation: String,
  normalDownloads: Number,
  normalFilesAccessed: Number,
  normalSessionDuration: Number,
}

That baseline becomes the reference point for anomaly analysis.

Every monitoring decision flows through that contextual layer.

Why I Chose Persistent Behavioral Memory

The temptation when building security systems is to continuously add more rules.

More thresholds.
More heuristics.
More alert types.

That approach eventually collapses under its own complexity.

I wanted the opposite.

The system should become more accurate by remembering behavior, not by endlessly multiplying static conditions.

That design philosophy aligned closely with ideas from Vectorize’s work on agent memory systems.

What interested me was not the chatbot side of memory systems. It was the architectural principle:

  • contextual persistence
  • historical continuity
  • evolving state awareness

That maps surprisingly well to insider threat monitoring.

A monitoring engine that remembers behavioral history can reason differently from one that only evaluates isolated events.

That became the core technical through-line for the entire system.

Building the Monitoring Engine

The simulation engine eventually evolved into a contextual behavioral analysis pipeline.

Every monitoring cycle:

  1. Fetches active employees
  2. Retrieves baseline behavior
  3. Generates or receives new activity
  4. Compares deviations against historical norms
  5. Calculates anomaly severity
  6. Updates trust score
  7. Creates alerts if thresholds are crossed
  8. Persists trust evolution history

The important part is that trust scores evolve continuously.

The system is intentionally stateful.

Here is the core monitoring logic:

const isSuspicious = Math.random() < 0.25;

let downloads = randomBetween(
  1,
  baseline.normalDownloads || 5
);

if (isSuspicious) {
  downloads =
    (baseline.normalDownloads || 5) +
    randomBetween(50, 300);

  anomalyScore = randomBetween(20, 50);
  trustImpact = -randomBetween(10, 25);
}

That looks simple, but it changed the entire behavior of the system.

The monitoring engine stopped behaving like a random alert generator and started behaving like a contextual anomaly detector.

The distinction matters.

Why Trust Scores Became More Important Than Alerts

One of the most useful decisions I made was treating trust score evolution as a first-class system primitive.

Most monitoring systems optimize around alerts.

I optimized around behavioral drift.

An employee trust score is continuously updated based on:

  • anomaly severity
  • historical deviations
  • suspicious activity frequency
  • behavioral consistency

That creates a much more understandable operational model.

A single suspicious event rarely matters.

Sustained behavioral deviation does.

That evolution is persisted using a dedicated trust history collection:

await TrustHistory.create({
  employeeId: employee._id,
  score: newScore,
  changeReason: action,
});

Persisting that historical trajectory ended up being incredibly useful for explainability.

The frontend can visualize not just the current state, but the behavioral journey that produced it.

That became one of the strongest parts of the system.

The Frontend Was Harder Than the Backend

The backend logic was relatively straightforward.

The frontend was where things became painful.

Enterprise dashboards sound simple until you try to make:

  • large activity tables
  • analytics charts
  • live feeds
  • expandable forensic panels
  • mobile responsiveness
  • dynamic updates

all coexist without collapsing into layout chaos.

The Activity Logs page alone became surprisingly complex.

It combines:

  • real-time monitoring feed
  • filtering
  • live updates
  • anomaly analytics
  • expandable investigation panels
  • chart visualizations

The chart layer introduced its own set of problems.

Recharts in responsive enterprise layouts can become fragile very quickly.

One missing container width or invalid flex constraint and the entire visualization stack explodes with:

The width(-1) and height(-1) of chart should be greater than 0

The eventual fix was embarrassingly simple:

<div className="w-full h-[300px] min-w-0">
  <ResponsiveContainer width="100%" height="100%">

That min-w-0 ended up mattering far more than it should.

Frontend layout systems remain one of software engineering’s stranger forms of emotional warfare.

Integrating Investigation Workflows

The most interesting UX decision was connecting Threat Center investigations directly into employee monitoring pages.

Originally, I considered building a separate investigation route.

That turned out to be unnecessary.

The employee monitoring page already contained:

  • trust evolution
  • behavioral analytics
  • anomaly history
  • suspicious activities
  • AI reasoning
  • session analysis

So the investigation workflow became:

router.push(
  `/dashboard/employees/${selectedThreat.employeeId}`
)

That small routing decision dramatically improved the operational flow.

The system suddenly felt coherent.

Threats were no longer disconnected alerts. They became entry points into a deeper behavioral investigation system.

That architectural simplification ended up making the product feel more realistic.

Using cascadeflow-Style Runtime Orchestration

Another important influence came from studying cascadeflow’s orchestration model.

What I found useful was the idea of structured execution flow instead of chaotic asynchronous handling.

Insider threat monitoring naturally becomes event-heavy:

  • activity updates
  • trust recalculations
  • anomaly scoring
  • alert escalation
  • UI refreshes
  • session isolation

Without orchestration discipline, those flows become difficult to reason about.

I adapted a simplified orchestration pattern inspired by cascadeflow concepts:

Activity Event
→ Baseline Comparison
→ Anomaly Analysis
→ Trust Update
→ Alert Evaluation
→ Session Response
→ Historical Persistence

That sequence became the operational backbone of the platform.

The frontend mirrors the same structure visually.

That consistency between backend flow and UI behavior reduced complexity significantly.

Example Threat Investigation Flow

A typical incident now looks like this:

  1. Employee logs in at 03:12 AM
  2. Device mismatch detected
  3. Download volume exceeds behavioral baseline
  4. Session duration becomes abnormal
  5. Trust score drops significantly
  6. Threat generated in Threat Center
  7. Analyst opens investigation
  8. Employee behavioral history loads
  9. AI reasoning panel explains deviations
  10. Session isolation triggered automatically

The important thing is that the system explains why something became suspicious.

That explainability layer matters far more than adding more AI terminology.

Security analysts care about reasoning quality, not branding language.

Lessons Learned

1. Stateful Systems Produce Better Security Signals

Most alert fatigue comes from systems forgetting context.

Persistent behavioral memory dramatically improves signal quality.

That ended up being the single most important architectural decision in the entire platform.

2. Trust Evolution Is More Useful Than Static Severity

A continuously evolving trust model is easier to reason about than isolated critical alerts.

Behavioral drift matters more than one-off incidents.

3. Explainability Is Not Optional

If a system cannot explain why something is suspicious, analysts eventually stop trusting it.

The AI reasoning panel became essential.

Not because it looked impressive, but because it reduced ambiguity.

4. Frontend Stability Matters More Than Fancy Features

A broken SOC dashboard destroys confidence instantly.

Responsiveness, table stability, chart reliability, and graceful API failure handling ended up mattering more than adding more features.

5. Persistent Memory Concepts Apply Far Beyond Chatbots

The most interesting thing I learned from studying systems like Hindsight was that memory architectures are not just useful for conversational systems.

They are broadly applicable anywhere contextual continuity matters.

Insider threat monitoring turned out to be a very strong fit.

Closing Thoughts

Building InsiderShield changed how I think about monitoring systems.

Most security tooling still treats behavior as disconnected events.

I increasingly think that model is fundamentally limited.

Systems that continuously remember context, evolve trust over time, and reason using behavioral continuity produce far more useful operational intelligence.

The interesting part is that the technical building blocks for this already exist.

Persistent memory systems, contextual orchestration models, and historical reasoning pipelines are no longer experimental ideas.

They are practical engineering tools.

The difficult part is integrating them coherently into real operational workflows.

That integration work ended up being far more interesting than the anomaly detection itself.

And honestly, far more difficult too.

The standard advice on handling bias in tech is wrong

Every woman who has worked in tech long enough has gotten the same three pieces of advice. Confront the behavior in the moment. Build an allyship network. Escalate to HR if it crosses a line.

I have tried all three. The first one cost me a working relationship I needed. The second one created a debt I never asked for. The third one put my name on a list and changed nothing about the person who did the thing.

This post is about what I do instead.

To be clear about scope: I am a Senior Developer Educator who has worked across data science, AI/ML, and developer education at multiple companies. My experience covers technical IC work and content-focused roles. If you are a manager of a 200-person org, the calculus may be different. If you are early career and still building a reputation, parts of this will not apply yet.

Here is what I have learned.

Confronting in the moment makes you the problem

The advice goes like this. Someone interrupts you, takes credit for your work, or makes a comment that lands wrong. You name it on the spot. “I was still speaking.” “That was my idea from the sync last Tuesday.” Direct, calm, professional.

The intent is good. The math does not work.

Confrontation in the moment costs you the room. Whatever you were trying to accomplish in that meeting is now derailed. You become the person who made the meeting awkward. The other party almost never says “you are right, I apologize.” They explain, they soften, they reframe. The meeting moves on. You have spent your credibility to teach a lesson nobody asked to learn.

Worse, it lodges in the calibration discussion. Six months later, when someone is evaluating you for a stretch project, the data point that surfaces is “she can be difficult in meetings.” Not the original behavior. Your response to it.

I am not saying never push back. I am saying the price is real and it falls on you, not on the person who caused the problem. Spend that currency on the things that matter. The interruption in a five-person room where the work product is at stake is worth a response. The interruption in a 30-person all-hands is not.

Allyship asks create the wrong dependency

The advice here goes like this. Find a senior man who will amplify your voice in meetings, vouch for your ideas, interrupt the interrupter on your behalf. Allies are powerful. Find them.

I have had good allies. They have made real differences. The problem is the structure.

When your visibility in a room depends on a man being in the room and choosing to use his voice for you, you have outsourced your authority. The ally goes on vacation. The ally moves to a different team. The ally has his own promotion cycle and is suddenly less available. Your visibility goes with him.

There is also a quieter cost. Performative allyship is its own ecosystem. Some allies advocate in ways that benefit their own brand more than they shift the dynamic. They make a visible show of speaking up, the room notices the ally, and you become a supporting character in someone else’s narrative arc. Your idea becomes the thing the ally championed.

The version of this that works is mutual. You have a peer or near-peer who knows your work cold, and you do the same for them, and you happen to mention each other’s contributions because you have actually been in the trenches together. That is not allyship as it is sold. That is normal professional credit, exchanged between people who respect each other.

HR works for the company

This one is the hardest to say out loud, because it sounds cynical and people want to believe HR is on their side.

HR exists to manage legal and reputational risk for the company. When that risk aligns with protecting you, they will protect you. When it does not, they will manage you. Both things can be true of the same HR person on the same day.

When you escalate to HR, three things happen at once. You go on a list. The other party gets coached, usually lightly. Your manager finds out, even if you were told it would be confidential, because your manager has to be looped in for any next-round decisions that involve you. The dynamic with your manager is permanently different from that moment on. They are now managing both you and a known issue, and they are evaluating which one is easier to make go away.

This does not mean never go to HR. It means go to HR with clear eyes. Use it when the behavior is severe enough that you would leave over it anyway, or when you need a paper trail because you have already decided to sue. Do not use it as the first or second line of response. The cost-benefit only works at the extremes.

What I actually do

The replacement playbook is less dramatic. It is also more effective, at least for me.

Document everything for yourself

I keep a private file. Date, what happened, who was there, my response if any. I do not write it for HR or for a future case. I write it so that I can see patterns.

Gender dynamics are gaslighting-prone. The behavior is often plausibly deniable in any single instance. You start to wonder if you are imagining it. The file makes that question answerable. 12 interruptions across four meetings with the same person is a pattern. One is a moment.

The file also helps you decide. When I review it monthly, I notice which incidents still feel hot and which ones I have moved past. The ones still hot get attention. The ones I have moved past taught me they were not worth the calories.

Build influence outside the orbit

The most powerful move I know is to make your reputation depend on people who are not in the dynamic. Skip-levels. Adjacent teams. External community. Public work.

If your visibility comes from your manager and your immediate peers, then any bias in that small group has outsized weight. If your visibility also comes from a talk you gave at a conference, a tutorial that is the top result for a search query, a working relationship with a VP two levels up, that small group’s opinion is one data point among many.

This is also the highest-value move you can make for your career independent of any bias. The bias problem and the career problem have the same solution.

Choose what to ignore on purpose

Most micro-incidents are not worth a response. Picking your battles is not surrender. It is portfolio management.

The principle I use: respond to things that have ongoing operational cost, ignore things that are purely social. Someone taking credit for your work in front of your skip-level has operational cost. Someone explaining your own field to you at a happy hour does not. The first shapes future decisions about your work. The second is a bad time you can walk away from.

The energy you save here is what you spend on the work that compounds. Shipping the thing. Writing the post. Giving the talk. The work is what creates room to maneuver.

Leave well, not loud

Sometimes the answer is to go.

When that is the answer, leaving well matters more than leaving loud. A loud exit feels righteous. It also makes every future hiring manager nervous. Leaving well means quiet, on your timeline, with the relationships you want to keep intact. Tell the people who matter to you that you are going. Do not tell the people who caused the problem anything they have not earned.

The best revenge, if you want to think of it that way, is to show up two years later with a better title at a better company and have your work cited in the room you left. The loud exit forecloses on that future. The quiet exit keeps it open.

The underlying trade

The standard advice positions the woman experiencing bias as the agent of correction. Confront him. Recruit allies for yourself. Escalate up the chain. All of it is work, all of it is risky, all of it is uncompensated.

The replacement is to stop trying to fix the dynamic and start building standing that makes the dynamic less relevant. None of this fixes the structural problem. The structural problem will outlast any individual woman’s career strategy. What this does is keep you operating at full capacity inside an imperfect system, while the slow work of fixing the system happens at the pace it happens.

That is the trade I have made. Your trade may look different.