Back to flin
flin

Rate Limiting and Security Headers

How FLIN provides built-in rate limiting with sliding windows and automatic security headers on every response -- protecting applications against abuse, XSS, clickjacking, and MIME sniffing by default.

Thales & Claude | March 25, 2026 7 min flin
flinrate-limitingheaderssecurity

Two security features that every web application needs and most developers forget to add: rate limiting to prevent abuse, and security headers to prevent client-side attacks. In the Node.js ecosystem, rate limiting requires express-rate-limit (or rate-limiter-flexible, or bottleneck). Security headers require helmet (or manual configuration of a dozen headers). Both must be installed, configured, and maintained separately.

FLIN builds both into the runtime. Rate limiting is available as a guard or middleware function. Security headers are added to every response automatically. No installation. No configuration. No way to forget.

Rate Limiting: The Guard

The simplest way to add rate limiting is the rate_limit guard:

// app/api/auth/login.flin

guard rate_limit(5, 60) // 5 requests per 60 seconds

route POST { // Login logic -- only 5 attempts per minute per IP } ```

Two parameters: the maximum number of requests and the window duration in seconds. If a client exceeds the limit, they receive:

HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711411245

{"error": "Too many requests. Try again in 45 seconds.", "status": 429} ```

The response includes standard rate limit headers so clients can implement backoff logic, and a Retry-After header indicating when the limit resets.

Rate Limiting: The Middleware Function

For more flexible rate limiting, use the rate_limit() function in middleware:

// app/api/_middleware.flin

middleware { rate_limit(request.ip, limit: 100, window: 60)

response.headers["X-RateLimit-Limit"] = "100" response.headers["X-RateLimit-Remaining"] = to_text(rate_remaining(request.ip))

next() } ```

The middleware function allows custom rate limit keys. You can rate limit by IP, by user, by API key, or by any combination:

// Rate limit by user instead of IP (for authenticated endpoints)
middleware {
    if request.user != none {
        rate_limit(to_text(request.user.id), limit: 1000, window: 60)
    } else {
        rate_limit(request.ip, limit: 100, window: 60)
    }
    next()
}

This pattern gives authenticated users a higher limit (1000/min) while keeping unauthenticated requests at a stricter limit (100/min).

The Sliding Window Algorithm

FLIN's rate limiter uses a sliding window algorithm that is more accurate than fixed windows:

pub struct RateLimiter {
    windows: HashMap<String, SlidingWindow>,
}

pub struct SlidingWindow { current_count: u32, previous_count: u32, window_start: Instant, window_duration: Duration, max_requests: u32, }

impl SlidingWindow { pub fn check(&mut self) -> RateLimitResult { let now = Instant::now(); let elapsed = now.duration_since(self.window_start);

if elapsed >= self.window_duration { // Rotate window self.previous_count = self.current_count; self.current_count = 0; self.window_start = now; }

// Weighted estimate of requests in the sliding window let weight = 1.0 - (elapsed.as_secs_f64() / self.window_duration.as_secs_f64()); let estimated = (self.previous_count as f64 * weight) + self.current_count as f64;

if estimated >= self.max_requests as f64 { let retry_after = self.window_duration - elapsed; RateLimitResult::Exceeded { retry_after } } else { self.current_count += 1; let remaining = self.max_requests - estimated.ceil() as u32; RateLimitResult::Allowed { remaining } } } } ```

The sliding window provides a smooth rate limit instead of the "burst at window boundary" problem that fixed-window algorithms suffer from. A client that sends 5 requests at 0:59 and 5 more at 1:01 is correctly limited, even though each individual minute contains only 5 requests.

Tiered Rate Limits

Different endpoints often need different rate limits. FLIN supports this naturally through the guard system:

// app/api/auth/login.flin
guard rate_limit(5, 60)      // Strict: 5/min for login attempts

// app/api/auth/register.flin guard rate_limit(3, 3600) // Very strict: 3/hour for registration

// app/api/products.flin guard rate_limit(100, 60) // Generous: 100/min for product listing

// app/api/search.flin guard rate_limit(30, 60) // Moderate: 30/min for search (expensive) ```

Each guard maintains its own counter. A user who exhausts their search limit can still access products. The limits are independent and scoped to each endpoint.

Automatic Security Headers

Every HTTP response from a FLIN application in production mode includes a set of security headers. These are not optional. They are not configurable (though they can be extended). They are part of every response.

X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'
Permissions-Policy: camera=(), microphone=(), geolocation=()

Each header protects against a specific class of attack:

X-Frame-Options: DENY

Prevents clickjacking attacks where a malicious site embeds your application in an iframe and tricks users into clicking hidden buttons. With DENY, your pages cannot be framed at all.

X-Content-Type-Options: nosniff

Prevents MIME type sniffing. Without this header, a browser might interpret a text file as JavaScript if it contains valid JS syntax, enabling XSS attacks through file uploads.

Strict-Transport-Security

Forces HTTPS for all future requests. Once a browser sees this header, it will never make an HTTP request to your domain for the next year. The includeSubDomains directive extends this to all subdomains, and preload allows inclusion in browser preload lists.

Content-Security-Policy

Controls which resources the browser is allowed to load:

default-src 'self'       -- Only load resources from the same origin
script-src 'self'        -- Only execute scripts from the same origin
style-src 'self' 'unsafe-inline' -- Styles from same origin + inline styles

This blocks all external scripts, iframes, fonts, and other resources. A XSS vulnerability that injects