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:

flin// 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:

httpHTTP/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:

flin// 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:

flin// 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:

rustpub 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:

flin// 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.

httpX-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 <script src="evil.com/steal.js"> is neutralized because the browser refuses to load scripts from external origins.

Permissions-Policy

Disables browser features that are commonly abused:

camera=()        -- No page can access the camera
microphone=()    -- No page can access the microphone
geolocation=()   -- No page can access location data

These defaults are restrictive. If a FLIN application needs camera access (for a QR scanner, for example), the developer can override these in flin.config.

Customizing Security Headers

While the defaults are appropriate for most applications, they can be extended or overridden:

flin// flin.config

security {
    headers {
        csp = "default-src 'self'; script-src 'self' https://cdn.example.com; img-src 'self' data: https:"
        frame = "sameorigin"

        hsts {
            max_age = 31536000
            include_subdomains = true
            preload = true
        }

        custom = {
            "X-Custom-Header": "value"
        }
    }
}

Note that you can only make the policy more permissive, not less. You cannot disable X-Content-Type-Options or remove Strict-Transport-Security. The baseline security is non-negotiable.

Development vs Production

In development mode, some security headers are relaxed to aid debugging:

Development:
- HSTS is not sent (you are on localhost)
- CSP allows 'unsafe-eval' (for dev tools)
- X-Frame-Options allows sameorigin (for dev preview)

Production:
- Full security headers on every response
- No exceptions, no overrides for the baseline

The FLIN runtime detects the mode from the FLIN_MODE environment variable or from the presence of .env.development vs .env.production.

DDoS Mitigation

Rate limiting alone does not stop a distributed denial-of-service attack (thousands of IPs each sending a few requests). But it does mitigate application-level attacks:

flin// Expensive endpoint protection
guard rate_limit(10, 60)

route POST "/search" {
    // Full-text search is CPU-intensive
    results = search query.q in Article by content limit 20
    results
}

Without rate limiting, a single attacker could send 1000 search requests per second and bring the server to its knees. With rate limiting, each IP is capped at 10 per minute. The server stays responsive for legitimate users.

For network-level DDoS, FLIN applications should be deployed behind a CDN or reverse proxy (Cloudflare, Caddy, Nginx) that handles TCP-level protection. Rate limiting in FLIN protects the application layer.

The Compound Effect

Rate limiting and security headers are individually simple. Their value is in the compound effect:

  • Rate limiting prevents brute force -> account lockout prevents credential stuffing -> timing-safe comparison prevents timing attacks -> Argon2 prevents offline cracking. Each layer makes the attack harder.
  • CSP prevents XSS -> nosniff prevents MIME confusion -> HSTS prevents downgrade attacks -> frame-ancestors prevents clickjacking. Each header closes a different attack vector.

When every FLIN application gets these protections by default, the entire ecosystem is hardened. A new developer deploying their first FLIN application gets the same security baseline as an experienced developer who has been configuring these headers for a decade.

In the next article, we cover two-factor authentication -- how FLIN implements TOTP (Time-Based One-Time Passwords) as a built-in feature.


This is Part 109 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language from scratch.

Series Navigation: - [108] JWT Authentication in 3 Lines of FLIN - [109] Rate Limiting and Security Headers (you are here) - [110] Two-Factor Authentication (TOTP) - [111] OAuth2 and Social Authentication

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles