<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->timestamp('last_seen_at')->nullable()->index()->after('remember_token');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('last_seen_at');
});
}
};
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;
class UpdateLastSeen
{
private const THROTTLE_SECONDS = 60;
public function handle(Request $request, Closure $next): Response
{
$user = Auth::user();
if ($user) {
$this->touch($user);
}
return $next($request);
}
private function touch($user): void
{
$cacheKey = 'last-seen:' . $user->getAuthIdentifier();
// Cache::add is atomic: it only succeeds once per throttle window.
if (! Cache::add($cacheKey, true, self::THROTTLE_SECONDS)) {
return;
}
$user->forceFill(['last_seen_at' => Carbon::now()])->saveQuietly();
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Carbon;
class User extends Authenticatable
{
private const ONLINE_THRESHOLD_MINUTES = 5;
protected $casts = [
'last_seen_at' => 'datetime',
];
public function scopeOnline(Builder $query): Builder
{
return $query->where(
'last_seen_at',
'>=',
Carbon::now()->subMinutes(self::ONLINE_THRESHOLD_MINUTES)
);
}
public function isOnline(): bool
{
if (is_null($this->last_seen_at)) {
return false;
}
return $this->last_seen_at->greaterThanOrEqualTo(
Carbon::now()->subMinutes(self::ONLINE_THRESHOLD_MINUTES)
);
}
}
<?php
use App\Http\Middleware\UpdateLastSeen;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
// Only session-authenticated web traffic should record presence.
$middleware->web(append: [
UpdateLastSeen::class,
]);
})
->create();
Recording when a user was last active sounds trivial — update a last_seen_at column on every request — but doing it naively writes to the database on every authenticated hit, which turns a cheap read-heavy app into a write-heavy one. This snippet shows the common production compromise: track activity in middleware, but throttle the actual database write so it happens at most once per interval per user.
The 2024_add_last_seen_to_users migration adds a nullable last_seen_at timestamp and indexes it, since presence queries ("who was online in the last 5 minutes") filter on that column. Keeping it nullable matters because existing rows and never-logged-in accounts legitimately have no value.
UpdateLastSeen middleware is the heart of the pattern. It runs after resolving the authenticated user via Auth::user(), then uses Cache::add() as a lightweight distributed lock: add only writes the key if it does not already exist and returns false otherwise, so within the THROTTLE_SECONDS window the middleware short-circuits and skips the write entirely. This collapses hundreds of requests into a single UPDATE. The middleware also defers work to $next($request)->send-adjacent flow by touching the timestamp before returning the response, and it swallows the case of guest requests by returning early when $user is null. Writing through forceFill() plus saveQuietly() avoids firing model events and updated_at churn that unrelated observers might react to.
User model exposes the domain concept rather than raw column access: scopeOnline() builds the "active within N minutes" query using where('last_seen_at', '>=', ...), and isOnline() answers the boolean for a single record. Centralizing the threshold here keeps the definition of "online" in one place.
The trade-off is deliberate: last-seen becomes eventually consistent and coarse-grained (accurate to within the throttle window), which is perfectly acceptable for presence UI. The Redis-backed cache guard means the throttle works correctly across multiple app servers, unlike an in-process static flag. A pitfall to watch is cache eviction — if the cache is flushed, writes resume immediately, which is safe but slightly chattier. Registering the middleware in the web group ensures it only runs for session-authenticated traffic.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.alert = false
Bullet.bullet_logger = true
Bullet.console = true
N+1 query detection with Bullet gem
export type Settled<R> =
| { status: 'fulfilled'; value: R }
| { status: 'rejected'; reason: unknown };
export interface ConcurrencyOptions {
limit: number;
Simple concurrency limiter for batch operations
json.array! @posts do |post|
json.cache! ['v1', post], expires_in: 1.hour do
json.id post.id
json.title post.title
json.excerpt post.excerpt
json.published_at post.published_at
Fragment caching for expensive JSON serialization
Share this code
Here's the card — post it anywhere.