Embedding vastlint in an SSP, DSP, or ad server
vastlint is built for in-process validation inside ad tech infrastructure. Embed vastlint-core directly in your bid handler, SSAI stitcher, or ad server to validate every VAST response before committing the impression — no subprocess, no network round-trip, no external dependency to manage.
A typical OpenRTB bid cycle has 100–300 ms to work with. vastlint validates a 17 KB production VAST tag in 363 µs on a single core, adding less than 2.1% of the bid budget on the heaviest 44 KB tags. An SSAI platform doing 1,000 stitches/sec spends more time on DNS than on VAST validation.
Architectural diagram
VAST ad flow: wrapper chain to viewer
Why validate at bid time?
VAST errors are silent. A missing <Impression>, a malformed duration, a VPAID tag on a CTV device — none of these produce a player error you can act on. The impression is lost, the tracking fires (or doesn't), and a discrepancy surfaces in a report three days later. By the time you see it, the revenue is gone.
Validating inline, before the bid is committed, lets you:
- Reject invalid tags and serve a fallback in the same auction window
- Return structured rule IDs to the demand partner so they can fix the creative at the source
- Track per-partner error rates and escalate when they spike
- Filter by revenue-impact rules to avoid false positives on advisory-only issues
Rust — vastlint-core
vastlint-core is a zero-dependency Rust library. Three compile-time dependencies (quick-xml, url, phf), no runtime dependencies. Rules are compiled Rust functions — no regex engine, no schema interpreter, no allocator pressure beyond the parse.
# Cargo.toml
[dependencies]
vastlint-core = "0.4"Basic validation:
use vastlint_core::validate;
let result = validate(vast_xml);
if !result.summary.is_valid() {
for issue in &result.issues {
// issue.id — stable rule ID (e.g. "VAST-2.0-inline-impression")
// issue.severity — "error", "warning", or "info"
// issue.message — human-readable description
// issue.path — XPath location in the document
eprintln!("[{}] {} — {}", issue.severity, issue.id, issue.path);
}
}With per-deployment rule overrides (e.g. relax a CTV-only rule for web inventory):
use std::collections::HashMap;
use vastlint_core::{validate_with_context, RuleLevel, ValidationContext};
let mut overrides = HashMap::new();
overrides.insert("VAST-4.1-mezzanine-recommended", RuleLevel::Off);
overrides.insert("VAST-2.0-mediafile-https", RuleLevel::Error); // promote to error
let ctx = ValidationContext {
rule_overrides: Some(overrides),
..Default::default()
};
let result = validate_with_context(vast_xml, ctx);Full API documentation is on docs.rs/vastlint-core.
Go — vastlint-go (no Rust toolchain required)
vastlint-go wraps the same Rust core via CGo. Prebuilt static libraries are bundled in the module — you do not need a Rust toolchain on your build or production machines. Supported platforms: Linux (amd64, arm64), macOS (amd64, arm64).
go get github.com/aleksUIX/vastlint-goBasic validation:
import vastlint "github.com/aleksUIX/vastlint-go"
result, err := vastlint.Validate(xmlBytes)
if err != nil {
log.Fatal(err)
}
if !result.Valid {
for _, issue := range result.Issues {
log.Printf("[%s] %s: %s", issue.Severity, issue.ID, issue.Message)
}
}With wrapper depth cap and per-deployment overrides:
result, err := vastlint.ValidateWithOptions(xmlBytes, vastlint.Options{
MaxWrapperDepth: 5, // IAB-recommended maximum
RuleOverrides: map[string]string{
"VAST-4.1-mezzanine-recommended": "off", // web inventory, no SSAI
"VAST-2.0-mediafile-https": "error", // enforce HTTPS strictly
},
})
if err != nil || !result.Valid {
// Quarantine the tag. Return result.Issues to the demand partner.
}Elixir / Erlang — vastlint-erlang
vastlint-erlang supports two integration modes. For production ad delivery, use the OTP port mode — vastlint-cli runs as a supervised OS process, so a crash or panic in the Rust layer is fully isolated and never affects the BEAM node. The NIF mode is available as an opt-in for non-critical paths.
OTP port mode — recommended for production ad delivery
Spawn vastlint daemon as an Erlang port under a NimblePool supervisor. The port communicates over stdin/stdout with newline-delimited JSON. A worker crash is isolated — the pool supervisor restarts it transparently. Add vastlint-cli to your deployment and nimble_pool to your mix.exs:
# mix.exs
defp deps do
[{:nimble_pool, "~> 1.0"}]
enddefmodule MyApp.VastValidator do
use NimblePool
@cli_bin System.find_executable("vastlint") ||
raise("vastlint binary not found on PATH")
def start_link(opts \\ []) do
NimblePool.start_link(worker: {__MODULE__, opts},
pool_size: System.schedulers_online(),
name: __MODULE__)
end
def validate(xml, timeout \\ 5_000) do
NimblePool.checkout!(__MODULE__, :checkout, fn _from, port ->
{call(port, xml, timeout), port}
end, timeout)
end
@impl NimblePool
def init_worker(_opts) do
port = Port.open({:spawn_executable, @cli_bin},
[:binary, :use_stdio, {:packet, 4},
args: ["daemon"]])
{:ok, port}
end
@impl NimblePool
def handle_checkout(:checkout, _from, port, pool_state),
do: {:ok, port, port, pool_state}
@impl NimblePool
def handle_checkin(port, _from, port, pool_state),
do: {:ok, port, pool_state}
@impl NimblePool
def terminate_worker(_reason, port, _pool_state) do
Port.close(port)
:ok
end
defp call(port, xml, timeout) do
# {:packet, 4} handles framing automatically — send raw XML binary.
Port.command(port, xml)
receive do
{^port, {:data, json}} -> Jason.decode!(json, keys: :atoms)
after
timeout -> {:error, :timeout}
end
end
endcase MyApp.VastValidator.validate(xml_string) do
%{valid: true} -> :ok
%{issues: issues} -> {:reject, issues}
{:error, reason} -> {:error, reason}
endNIF mode — opt-in
The DirtyCpu NIF is available for non-critical paths where the ~10–50 µs port overhead matters. A NIF crash will take down the BEAM node — do not use this in any pipeline where node availability is required. Add the hex package to your mix.exs:
# mix.exs
defp deps do
[
{:vastlint, "~> 0.4", github: "aleksUIX/vastlint-erlang"}
]
endcase Vastlint.validate(xml_string) do
{:ok, %{summary: %{errors: 0}}} ->
:ok
{:ok, %{issues: issues}} ->
errors = Enum.filter(issues, &(&1.severity == "error"))
{:reject, errors}
{:error, reason} ->
{:error, reason}
endProduction patterns
Filter by revenue impact before rejecting
Not every error warrants a hard rejection. Use the rules reference to identify which rule IDs map to impression-killing defects (missing <Impression>, bad <Duration> format, no <MediaFile>) versus advisory issues. Run two passes: hard-reject on errors, quarantine-for-review on warnings.
Return rule IDs to the demand partner
Every issue in the result includes a stable id field (e.g. VAST-2.0-inline-impression) and an XPath path pointing to the exact element. Return these in your bid response or partner notification — they are actionable without manual triage.
Wrapper depth cap
The IAB recommends a maximum wrapper chain depth of 5. Set MaxWrapperDepth: 5 (Go) or the equivalent context field in Rust to enforce this. The rule VAST-2.0-wrapper-depth fires when the depth is exceeded.
Self-hosting (air-gapped deployments)
All bindings run entirely in-process. There is no network code in vastlint-core — no callbacks, no telemetry, no license checks. For teams that also need the web validator or REST API on-premise, the Docker image is FROM scratch, under 5 MB, with a cold-start under 10 ms.
Performance reference
Benchmarked on Apple M4 (10-core), production-realistic VAST tags:
| Tag size | Single-core throughput | Single-core latency | 10-core throughput |
|---|---|---|---|
| 17 KB | 2,747 tags/sec | 363 µs | 15,760 tags/sec |
| 44 KB | 475 tags/sec | 2,104 µs | 2,635 tags/sec |
A typical OpenRTB bid cycle takes 100–300 ms; validation adds less than 2.1% of that budget even on the heaviest tags.
Enterprise support & integration consulting
Evaluating vastlint for production use in a DSP bid pipeline, SSAI platform, ad server, or CTV device? The author is available for integration consulting, SLA-backed enterprise support agreements, custom rule development, and on-site architecture review.
Typical engagements include:
- Architecture review for in-process bid-time validation
- Custom rule development for proprietary spec extensions or internal trafficking requirements
- Per-partner error rate dashboards and alerting integration
- Priority issue resolution and private disclosure channel
- Air-gapped or AWS Marketplace deployment support
Email: aleks@vastlint.org
Or reach out through one of these channels:
- GitHub Discussions — questions, use-case sharing, and production feedback
- GitHub Issues — bug reports and feature requests
- Security Advisory — private disclosure channel, 48-hour acknowledgement SLA
- GitHub: @aleksUIX
Further reading
- Rules reference — rule details with severity, category, and fix examples
- How to validate VAST XML — overview of all integration options
- MCP server — for agentic pipelines and AI buyer agents
- vastlint-core API docs
- vastlint-go README
- vastlint-erlang README