The Ultimate Guide to IP Blocking & Rate Limiting in .NET: SQLite, In-Memory, and Redis Approaches

The Ultimate Guide to IP Blocking & Rate Limiting in .NET: SQLite, In-Memory, and Redis Approaches

Published on January 30, 2025

In today's digital world, preventing abusive traffic, spam, and potential attacks on your .NET application is essential. A well-implemented IP blocking and rate limiting mechanism ensures security, performance, and a seamless user experience.

This guide explores three powerful approaches to handling IP blocking and rate limiting in .NET:

  1. SQLite (Persistent, lightweight, memory-efficient)
  2. In-Memory (Fast, ephemeral, best for short-term rate limiting)
  3. Redis (Distributed, scalable, and highly performant)

By the end of this article, you will have a fully functional, scalable, and effective IP blocking mechanism tailored to your application's needs.

Let's Define Our Foe

  • Normal User: Low frequency, no action needed.
  • Suspicious User: Requests more frequently than a normal user but not aggressively.
  • πŸ‘‰ Solution: Temporary lock (e.g., Rate Limiting)
  • Malicious User: Very high request frequency.
  • πŸ‘‰ Solution: Permanent block (e.g., IP Blacklisting)

Define Measures


How to do it with SQLite?

Why SQLite?

βœ… Persistent storage across application restarts

βœ… Suitable for small to mid-sized applications

βœ… Ideal when RAM is limited and In-Memory caching is not feasible

Database Schema (SQLite)

CREATE TABLE IF NOT EXISTS IpRequests (
    Id INTEGER PRIMARY KEY AUTOINCREMENT,
    IpAddress TEXT UNIQUE,
    RequestCount INTEGER DEFAULT 1,
    LastRequest DATETIME DEFAULT CURRENT_TIMESTAMP,
    BlockedUntil DATETIME NULL
);

Middleware Implementation (SQLite)

public class SqliteIpRateLimitMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _connectionString = "Data Source=ip_tracking.db";

    public SqliteIpRateLimitMiddleware(RequestDelegate next)
    {
        _next = next;
        InitializeDatabase();
    }

    private void InitializeDatabase()
    {
        using var connection = new SqliteConnection(_connectionString);
        connection.Open();
        string query = @"CREATE TABLE IF NOT EXISTS IpRequests (
                        Id INTEGER PRIMARY KEY AUTOINCREMENT,
                        IpAddress TEXT UNIQUE,
                        RequestCount INTEGER DEFAULT 1,
                        LastRequest DATETIME DEFAULT CURRENT_TIMESTAMP,
                        BlockedUntil DATETIME NULL)";
        using var command = new SqliteCommand(query, connection);
        command.ExecuteNonQuery();
    }

    public async Task Invoke(HttpContext context)
    {
        string ip = context.Connection.RemoteIpAddress?.ToString();
        if (string.IsNullOrEmpty(ip)) { await _next(context); return; }

        using var connection = new SqliteConnection(_connectionString);
        connection.Open();

        string selectQuery = "SELECT RequestCount, LastRequest, BlockedUntil FROM IpRequests WHERE IpAddress = @IpAddress";
        using var selectCommand = new SqliteCommand(selectQuery, connection);
        selectCommand.Parameters.AddWithValue("@IpAddress", ip);

        using var reader = selectCommand.ExecuteReader();
        if (reader.Read())
        {
            int requestCount = reader.GetInt32(0);
            DateTime lastRequest = reader.GetDateTime(1);
            object blockedUntilObj = reader["BlockedUntil"];

            if (blockedUntilObj != DBNull.Value && Convert.ToDateTime(blockedUntilObj) > DateTime.UtcNow)
            {
                context.Response.StatusCode = 403;
                await context.Response.WriteAsync("You are temporarily blocked.");
                return;
            }

            requestCount = (DateTime.UtcNow - lastRequest).TotalMinutes > 1 ? 1 : requestCount + 1;
            string updateQuery = requestCount > 10 ?
                "UPDATE IpRequests SET BlockedUntil = datetime('now', '+10 minutes') WHERE IpAddress = @IpAddress" :
                "UPDATE IpRequests SET RequestCount = @RequestCount, LastRequest = CURRENT_TIMESTAMP WHERE IpAddress = @IpAddress";

            using var updateCommand = new SqliteCommand(updateQuery, connection);
            updateCommand.Parameters.AddWithValue("@RequestCount", requestCount);
            updateCommand.Parameters.AddWithValue("@IpAddress", ip);
            updateCommand.ExecuteNonQuery();
        }
        else
        {
            string insertQuery = "INSERT INTO IpRequests (IpAddress) VALUES (@IpAddress)";
            using var insertCommand = new SqliteCommand(insertQuery, connection);
            insertCommand.Parameters.AddWithValue("@IpAddress", ip);
            insertCommand.ExecuteNonQuery();
        }
        await _next(context);
    }
}

Register Middleware in Startup.cs or Program.cs

app.UseMiddleware<SqliteIpRateLimitMiddleware>();


How to do it with In-Memory Cache?

Why In-Memory?

βœ… Fastest approach for low-memory consumption

βœ… Ideal for APIs running on a single server

βœ… Ephemeral storage (data lost on app restart)

Implementation

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;


public class IpRateLimitMiddleware
{
    private readonly RequestDelegate _next;
    private static readonly MemoryCache _cache = new(new MemoryCacheOptions());
    private static readonly HashSet<string> _blacklist = new();
    private static readonly TimeSpan CheckInterval = TimeSpan.FromMinutes(1);
    
    public IpRateLimitMiddleware(RequestDelegate next)
    {
        _next = next;
    }


    public async Task Invoke(HttpContext context)
    {
        string ip = context.Connection.RemoteIpAddress?.ToString();
        if (string.IsNullOrEmpty(ip))
        {
            await _next(context);
            return;
        }
        if (_blacklist.Contains(ip))
        {
            context.Response.StatusCode = 403; // Forbidden
            await context.Response.WriteAsync("Access denied.");
            return;
        }
        var requestCount = _cache.GetOrCreate(ip, entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = CheckInterval;
            return 0;
        });
        if (requestCount >= 30)
        {
            _blacklist.Add(ip);
            context.Response.StatusCode = 403;
            await context.Response.WriteAsync("You are permanently blocked.");
            return;
        }
        else if (requestCount >= 10)
        {
            context.Response.StatusCode = 429; // Too Many Requests
            await context.Response.WriteAsync("Too many requests. Try again later.");
            return;
        }
        _cache.Set(ip, requestCount + 1);
        await _next(context);
    }
}

Register Middleware in Startup.cs or Program.cs

app.UseMiddleware<SqliteIpRateLimitMiddleware>();


How to do it with Redis?

Why Redis?

βœ… Distributed caching for multiple servers

βœ… Handles millions of requests efficiently

βœ… Persists across application restarts

Implementation

using StackExchange.Redis;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;


public class RedisIpRateLimitMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ConnectionMultiplexer _redis;


    public RedisIpRateLimitMiddleware(RequestDelegate next, ConnectionMultiplexer redis)
    {
        _next = next;
        _redis = redis;
    }


    public async Task Invoke(HttpContext context)
    {
        string ip = context.Connection.RemoteIpAddress?.ToString();
        if (string.IsNullOrEmpty(ip)) { await _next(context); return; }


        var db = _redis.GetDatabase();
        string key = $"ip:{ip}";


        var requestCount = await db.StringIncrementAsync(key);
        if (requestCount == 1)
        {
            await db.KeyExpireAsync(key, TimeSpan.FromMinutes(1));
        }


        if (requestCount > 30)
        {
            await db.StringSetAsync($"blocked:{ip}", "true", TimeSpan.FromDays(1));
            context.Response.StatusCode = 403;
            await context.Response.WriteAsync("You are permanently blocked.");
            return;
        }
        else if (requestCount > 10)
        {
            context.Response.StatusCode = 429;
            await context.Response.WriteAsync("Too many requests. Try again later.");
            return;
        }


        await _next(context);
    }
}

Register Middleware in Startup.cs or Program.cs

app.UseMiddleware<RedisIpRateLimitMiddleware>();

Conclusion

Security is not a luxury; it's a necessity. With the increasing number of cyber threats and automated attacks, implementing a solid IP blocking and rate-limiting strategy is crucial for your .NET application.

By leveraging SQLite for persistence, In-Memory caching for speed, or Redis for scalability, you can strike the perfect balance between performance and security. No more server slowdowns, no more abuseβ€”just a seamless and efficient experience for legitimate users.

Like
Share