Skip to main content
Phisherman processes every URL through a multi-stage pipeline: a cache lookup, parallel checker execution, score aggregation, and a cache write. This page walks through each stage in detail.

Request lifecycle

The CheckerRegistry

CheckerRegistry is a small registry class that holds a list of Checker objects and runs them all in parallel.
Key properties of this design:
  • All checkers run concurrently via Promise.all — latency is bounded by the slowest checker, not the sum of all checkers.
  • Each checker races against a 2500 ms timeout via Promise.race. A timed-out checker contributes a score of 0 and does not fail the request.
  • Per-checker execution time is recorded in the timing map and returned in the ScanResult as executionTimeMs.

Checker registration

Checkers are registered in Scanner.ts at startup:
Each checker implements the Checker interface:

Result caching

Scan results are cached in Redis to avoid re-running the full checker pipeline for recently seen URLs.
Cache key — The URL is hashed with SHA-256:
Storage structure — To avoid Redis key explosion, all scan results are stored as fields of a single hash (scan_results). A companion sorted set (scan_results_expiry) stores each field ID with its expiry timestamp as the score, enabling efficient batch cleanup. Cache read — On a cache hit, Phisherman checks the exp field against Date.now(). Expired entries are deleted opportunistically before the fresh scan runs. Caching safe results — By default, URLs that resolve to a safe verdict are not cached, because they are high-volume and low-value to retain. Set SCAN_CACHE_SAFE_RESULTS=true to cache them:

Background feed refresh

CacheManager runs a background loop that keeps all threat feed data current. It is started once at server startup:
On each cycle, runAll() invokes every registered RefreshTask in sequence, then runs three cleanup routines: Each feed source registers its own refresh task with a source-specific interval:
Feed refresh is checked on every invocation — not on a separate per-source timer. The CacheManager loop fires every hour by default, but each source compares Date.now() against its own last-update timestamp and only refetches if its individual interval has elapsed.