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:
- SQLite (Persistent, lightweight, memory-efficient)
- In-Memory (Fast, ephemeral, best for short-term rate limiting)
- 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.