Savings Plan Amortized Cost in AWS Cost Explorer: What It Is and How to Use It

Savings Plan amortized cost converts a front-loaded or recurring Savings Plan fee into a daily rate you can actually reason about. The default unblended view shows what AWS billed your account each day so a 1-year All Upfront Savings Plan appears as a single large charge on Day 1, then silence for 364 days. Accurate for cash flow. Operationally useless for tracking commitment ROI, allocating costs to teams, or detecting underutilization.

Here’s what amortized cost is technically, how it differs from unblended and blended, and critically how to use it to catch unused commitment before it compounds.

What Is Savings Plan Amortized Cost?

Amortized cost is a cost dataset in AWS Cost Explorer and the Cost and Usage Report (CUR) that spreads Savings Plan fees uniformly across each day of the commitment term. It’s an accrual accounting view of your cloud spend.

The formula AWS applies:

Daily Amortized Fee = (Total Plan Fee) ÷ (Total Days in Term)

For a 1-year No Upfront Compute Savings Plan at $10/hour ($87,600/year):

  • Daily amortized fee: $87,600 ÷ 365 = $240/day regardless of whether EC2 usage consumed 100% or 60% of coverage that day

For a 1-year All Upfront plan at $80,000:

  • Unblended view: $80,000 on Day 1, $0 for Days 2–365

  • Amortized view: $219.18/day every day of the term

The effective cost per covered compute-hour in the amortized view is the true discounted rate, the number finance should use for chargebacks, and the number you should monitor to verify your Savings Plan is performing as expected.

The key operational difference:

An unblended view tells you what happened to your AWS bill. An amortized view tells you whether your Savings Plan is working.

If usage drops 40% in Month 6 on an All Upfront plan, the unblended view gives you no signal the $80,000 is already spent. In the amortized view, Month 6 shows unused commitment cost as a daily line item. That’s the waste signal.

If you want to understand how On-Demand, Reserved, and Spot pricing compares before committing, we covered the full breakdown here On-Demand vs Reserved vs Spot Instances: The Complete AWS Pricing Guide

How to View Amortized Cost in Cost Explorer

Prerequisites:

  • Cost Explorer enabled (Billing → Cost Explorer → Enable)
  • At least one active Savings Plan
  • Permissions: ce:GetCostAndUsage, ce:DescribeSavingsPlans

Step 1: AWS Console → Billing and Cost Management → Cost Explorer

Step 2: Set a date range covering your Savings Plan’s active period. 30 days minimum for utilization analysis.

Step 3: In the right-hand panel, open Advanced Options. The cost type defaults to Unblended.

Step 4: Select Amortized costs. If you have upfront Savings Plans, the previously lumpy daily cost line will flatten into a consistent daily rate.

Step 5: Group by dimension to see coverage distribution:

  • Group by Service → EC2 vs. Lambda vs. Fargate coverage
  • Group by Linked Account → which accounts received commitment coverage
  • Filter: Purchase Option = Savings Plans → isolate commitment cost from on-demand

Step 6: In the amortized view, AWS surfaces two Savings Plan line items:

  • Savings Plans recurring fee: daily amortized portion of the plan fee
  • Savings Plans upfront fee: if All/Partial Upfront, also shown as a daily amortized value

When usage fully covers the commitment, these appear as negative offsets against on-demand charges, netting to the discounted rate. When usage doesn’t cover the commitment, the unused portion appears as a positive cost with no offsetting savings. That’s your waste.

What Unused Commitment Actually Looks Like

Assume a Compute Savings Plan committing to $5/hour ($43,800/year, 1-year No Upfront).

Daily Usage vs. Commitment Outcome
100% Full $5/hour applies, zero unused commitment
70% $3.50/hour applied, $1.50/hour unused = $36/day wasted

50% $2.50/hour unused, $60/day = $1,800/month for nothing

None of this is visible as a daily signal in the unblended view. In the amortized view, unused commitment appears as a real-time daily cost line.

At scale: A 30% underutilized Compute Savings Plan on a $500,000/year commitment generates $150,000/year in unused commitment spend that produces zero AWS resources. At $6–12K/day in potential waste for larger environments, a single underutilization event undetected for 30 days is a $180K–$360K problem.

If you’re thinking through how to size and time your commitment purchases, we broke down the strategy here AWS Savings Plan Buying Strategy: Layering, Timing & Right-Sizing Commitment

Amortized Cost Across Consolidated Billing Accounts

In an AWS Organization, Savings Plans purchased in a management account can apply across all linked accounts. The amortized view is the only Cost Explorer dataset that properly attributes this coverage.

AWS applies the Savings Plan to usage across linked accounts (roughly by account ID, then by service). Each linked account’s amortized cost reflects the discounted rate for covered usage, regardless of which account owns the plan. Unused commitment is allocated back to the plan-owning account.

Why this matters for chargebacks: The unblended view shows the purchasing account bearing the full Savings Plan cost. The amortized view distributes that cost to accounts that actually consumed coverage. For any accurate internal cost allocation, you must use amortized cost unblended will systematically overcharge the management account and undercharge linked accounts.

Sample Athena query to surface unused commitment by day:

SELECT
line_item_usage_start_date AS usage_date,
savings_plan_savings_plan_a_r_n AS plan_arn,
SUM(savings_plan_unused_commitment) AS daily_unused_commitment,
SUM(savings_plan_used_commitment) AS daily_used_commitment,
ROUND(
SUM(savings_plan_unused_commitment) /
NULLIF(SUM(savings_plan_used_commitment + savings_plan_unused_commitment), 0) * 100, 2
) AS unused_pct
FROM your_cur_table
WHERE line_item_line_item_type = ‘SavingsPlanNegation’
OR line_item_line_item_type = ‘SavingsPlanCoveredUsage’
OR line_item_line_item_type = ‘SavingsPlanRecurringFee’
OR line_item_line_item_type = ‘SavingsPlanUpfrontFee’
GROUP BY 1, 2
ORDER BY 1 DESC, 3 DESC;

Verify CUR field names against your specific CUR configuration field availability depends on CUR version and configuration.

The most common CUR mistake: summing lineItem/UnblendedCost and savingsPlan/SavingsPlanEffectiveCost in the same aggregation. That’s double-counting. Choose one methodology and apply it consistently.

What the Amortized View Can’t Tell You

Amortized cost is necessary for commitment analysis but not sufficient for commitment management.

Latency: Cost Explorer data has a 24–72 hour refresh latency. Savings Plans utilization can lag by up to 3 days. At $6–12K/day in potential waste, a 72-hour refresh cycle means a utilization problem can compound to $18K–$36K before it appears in your dashboard.

No forward-looking signal: The amortized view shows historical commitment performance. It can’t tell you whether your current usage trajectory will overshoot or undershoot your commitment for the rest of the month.

No automated action: Cost Explorer shows you the problem of unused commitment as a daily cost line. Fixing it (adjusting commitment size, redistributing coverage, or recovering unused commitment value) requires a separate workflow.

Closing the Gap Between Signal and Action

Most teams review Savings Plans utilization monthly or quarterly. AWS Cost Explorer’s recommendation engine refreshes every 72+ hours. By the time a utilization drop appears, gets reviewed in a weekly FinOps meeting, and reaches a decision, 2–4 weeks of unused commitment may have accumulated.

Usage.ai refreshes commitment data every 24 hours. On a $500K/year Savings Plan with 20% underutilization, you’re burning ~$274/day in unused commitment and miss it for 3 days due to stale tooling, that’s $822 gone before anyone sees the signal.

The other differentiator: most vendors in this space issue credits when commitment goes unused. Credits apply against future AWS invoices. Usage.ai returns cash actual money back, not a discount on next month’s bill. Unused commitment stops being a sunk cost and becomes a recoverable one.

Setup operates at the billing layer with only no infrastructure changes, no access to production resources. Commitments are sized to actual baseline usage rather than peak, which directly reduces the unused commitment visible in your amortized cost view.

For FinOps teams managing a consolidated AWS Organization, the amortized view of unused Savings Plan commitment is the first diagnostic signal. The question is how fast you act on it.

How does your team currently monitor Savings Plan utilization Cost Explorer, CUR queries, a third-party tool, or something else?

For a deeper technical breakdown and additional insights, read the full article here → Understanding Savings Plan Amortized Cost in AWS Cost Explorer

How to Tailor Your Resume to a Job Description in 5 Minutes (A Method That Actually Works)

If you’ve ever heard “tailor your resume for every application” and immediately thought that sounds exhausting, you’re not alone. Most people skip tailoring entirely — or spend hours rewriting a document from scratch — because no one explains what targeted tailoring actually looks like.

Here’s the reality: for most applications, strong resume tailoring is a top-half edit, not a full rewrite. If your base resume is solid, you can tailor it to a specific job description in roughly five minutes. This guide walks through the method step by step.

The Core Insight: Recruiters Compare, They Don’t Read in a Vacuum

When a recruiter opens your resume, they’re not assessing it as a standalone document. They’re comparing it against the specific job posting they wrote. ATS systems work the same way at scale — they’re built to identify how well a resume matches a particular set of requirements.

A well-written generic resume can still underperform because it doesn’t reflect the vocabulary or priorities of the role. Tailoring fixes that without requiring a full rewrite.

The 5-Minute Method

Minute 1: Highlight the Repeated Keywords in the Job Description

Start with repetition. Pull up the job posting and mark everything that appears more than once:

  • Job title variations
  • Tools and technical skills
  • Core responsibilities
  • Outcome words (optimize, analyze, lead, scale, automate)
  • Domain terms (fintech, B2B SaaS, healthcare, compliance)

Don’t try to match every word. Focus on the 8–12 signals that actually define the role. For a software engineer posting, that might be: TypeScript, React, REST APIs, performance optimization, cross-functional collaboration.

Minute 2: Pick the 3 Requirements You Must Match

Not everything in a job description carries equal weight. Your goal is identifying the three requirements that would make a recruiter say “yes, this person looks plausible.”

Usually those are:

  1. The core function of the role
  2. The main tools or stack
  3. The business outcome or scope

For a product manager role, if the posting repeats customer research, roadmap prioritization, and stakeholder communication — those three drive your edits, even if the JD lists 15 other things.

Minute 3: Rewrite the Summary and Skills Section First

The top of your resume gets scanned first, which makes it the fastest win.

Before: “Software engineer with experience building web applications and backend systems.”

After: “Software engineer with experience building React and TypeScript applications, shipping REST API integrations, and improving performance for internal and customer-facing tools.”

The second version presents the same real experience in the language the hiring team is already using. Then tighten the skills section so your most relevant tools are visible without scrolling.

Minute 4: Rewrite Only the 4–6 Bullets That Carry the Most Weight

This is where most of the tailoring value lives. Target:

  • Your most recent role
  • Your most relevant project
  • One bullet that shows results
  • One bullet that shows ownership or cross-functional work

Before: “Worked on APIs and frontend features for internal systems.”

After: “Built React workflows and Node.js REST APIs for internal operations tools, reducing manual processing time by 28% and improving cross-team reporting reliability.”

The second version names the tools, provides a concrete outcome, and mirrors the way real job descriptions are written. That’s the pattern.

Minute 5: Remove Bullets That Dilute the Match

Tailoring isn’t only about adding keywords — it’s also about cutting noise.

Remove or shorten bullets that:

  • Don’t support the target role
  • Repeat the same kind of work
  • Use generic language without outcomes
  • Make your resume look unfocused

Most resumes actually improve when 10–20% of the content is removed. Less noise means the relevant material stands out more clearly.

The 5 Mistakes That Make Resume Tailoring Ineffective

1. Rewriting the whole resume from scratch. You don’t need a new document for every application. You need a focused edit.

2. Copy-pasting the job description. Mirroring language is good. Pasting the JD directly into your resume is obvious and unconvincing.

3. Ignoring the top third of the page. If your summary, title, and skills block still look generic, the whole resume will feel generic — even if the bullets are great.

4. Only changing the skills section. Keyword overlap helps with ATS. Bullets are what make the resume believable to a human recruiter.

5. Never checking the final version against the posting. Tailoring should end with a comparison, not a guess. Run a final ATS check before submitting.

A Quick Checklist Before You Submit

  • 8–12 key terms highlighted from the job description
  • 3 must-match requirements identified
  • Summary updated to reflect role language
  • Skills section reordered for relevance
  • 4–6 key bullets rewritten
  • Off-topic bullets removed or shortened
  • Final ATS comparison done

This takes about 5 minutes once you’ve done it a few times. The first run usually takes 10–15 minutes, and it gets faster every time.

One Tool Worth Using

If you want to move faster on the ATS comparison step, ManyOffer Resume Match lets you paste your resume and the job description side by side and immediately see which requirements, keywords, and phrasing are still missing.

Read the full article here

Been using ManyOffer to sharpen my own answers — if you want AI mock interviews with real LP feedback, they have a deal running through July worth checking out: Claim 1 free month here

Flutter vs React Native in 2026: I Built the Same App in Both

Flutter app and React Native app running side-by-side in iOS and Android simulators, both showing the same task list with identical data and a bottom navigation bar with three tabs

I spent three weekends building the same app twice. Not as an experiment — I had a real decision to make. We’re adding a mobile companion to one of our internal tools at work, a task management system that runs on Spring Boot microservices. My team has touched Dart before; I’m more comfortable with JavaScript. Neither of us had shipped serious mobile work in the last two years. The only honest way to pick a stack was to build something real in both frameworks and compare what I found.

The app I built isn’t a toy. It pulls tasks from a REST API, caches them locally with SQLite, fires scheduled push notifications when deadlines approach, and has smooth list animations on a bottom-nav layout with three tabs. Simple enough to build in a weekend. Complex enough to expose real differences between the two frameworks.

Here’s what I found — including a result that surprised me.

The App

Before comparing, let me be specific about what I built. Both versions:

  • Fetch tasks from a paginated REST endpoint with auth headers
  • Cache responses locally using SQLite
  • Support optimistic updates when marking a task complete
  • Schedule local push notifications 1 hour before each task’s due time
  • Use a bottom navigation bar with three tabs

This isn’t production code — it’s a controlled comparison. Both apps hit the same mock API running on my MacBook. Same UI design, same feature set, different frameworks.

Setup and Developer Experience

Flutter’s setup is more involved upfront. You run flutter doctor, chase down Android SDK versions, configure Xcode command-line tools. On a clean Mac it took me about 45 minutes to get a green build on both simulators. The error messages are usually specific enough to guide you, but it’s mechanical work.

React Native with Expo took 15 minutes. npx create-expo-app, pick a template, run npx expo start, scan the QR code. Done. The friction delta is real, especially if you’re onboarding a team that’s new to mobile development.

The catch: Expo’s managed workflow works brilliantly until you need a native module that isn’t in Expo’s SDK. Then you eject, and you’re roughly back to the same complexity as a bare React Native setup. For this test app, I stayed in managed Expo and hit no walls.

Flutter app and React Native app running side-by-side in iOS and Android simulators, both showing the same task list with identical data and a bottom navigation bar with three tabs

Writing UI Code

This is where the frameworks feel most different day-to-day.

Flutter uses widgets — everything is a widget, composable and explicit. Here’s the task list item:

class TaskTile extends StatelessWidget {
  final Task task;
  final VoidCallback onComplete;

  const TaskTile({super.key, required this.task, required this.onComplete});

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(
        task.title,
        style: task.completed
            ? const TextStyle(decoration: TextDecoration.lineThrough)
            : null,
      ),
      subtitle: Text(task.dueAt.toLocal().toString().substring(0, 16)),
      trailing: task.completed
          ? const Icon(Icons.check_circle, color: Colors.green)
          : IconButton(
              icon: const Icon(Icons.radio_button_unchecked),
              onPressed: onComplete,
            ),
    );
  }
}

React Native with TypeScript:

type TaskTileProps = {
  task: Task;
  onComplete: () => void;
};

export function TaskTile({ task, onComplete }: TaskTileProps) {
  return (
    <TouchableOpacity
      style={styles.row}
      onPress={task.completed ? undefined : onComplete}
    >
      <View style={styles.info}>
        <Text style={[styles.title, task.completed && styles.completed]}>
          {task.title}
        </Text>
        <Text style={styles.due}>{formatDate(task.dueAt)}</Text>
      </View>
      {task.completed ? (
        <Ionicons name="checkmark-circle" size={22} color="#4CAF50" />
      ) : (
        <Ionicons name="radio-button-off" size={22} color="#999" />
      )}
    </TouchableOpacity>
  );
}

Both are readable. The Flutter version is more verbose but more structured — you’re always in an explicit tree of typed widgets. The React Native version feels immediately familiar if you write React for the web, which is either an advantage or a liability depending on your team’s background.

One thing I noticed: Flutter’s hot reload is faster and more reliable than React Native’s throughout a full day of development. Not dramatically — but consistently.

State Management

I used Riverpod in Flutter and Zustand in React Native. Both are lightweight and explicit. I deliberately avoided Provider or Redux for a project this size.

Flutter with Riverpod:

final tasksProvider = AsyncNotifierProvider<TasksNotifier, List<Task>>(
  TasksNotifier.new,
);

class TasksNotifier extends AsyncNotifier<List<Task>> {
  @override
  Future<List<Task>> build() => _fetchTasks();

  Future<void> completeTask(String id) async {
    final previous = state;
    state = AsyncData(
      state.value!
          .map((t) => t.id == id ? t.copyWith(completed: true) : t)
          .toList(),
    );
    try {
      await TasksApi.complete(id);
    } catch (_) {
      state = previous;
    }
  }
}

React Native with Zustand:

interface TaskStore {
  tasks: Task[];
  loading: boolean;
  fetchTasks: () => Promise<void>;
  completeTask: (id: string) => Promise<void>;
}

export const useTaskStore = create<TaskStore>((set, get) => ({
  tasks: [],
  loading: false,
  fetchTasks: async () => {
    set({ loading: true });
    const tasks = await TasksApi.fetchAll();
    set({ tasks, loading: false });
  },
  completeTask: async (id) => {
    const previous = get().tasks;
    set({
      tasks: previous.map((t) => (t.id === id ? { ...t, completed: true } : t)),
    });
    try {
      await TasksApi.complete(id);
    } catch {
      set({ tasks: previous });
    }
  },
}));

The patterns are nearly identical — optimistic update, rollback on failure. The ergonomics are a wash. If you already know one of these libraries, it takes an afternoon to feel comfortable with the other.

Performance

This is where the narrative shifted for me.

Flutter renders through its own engine. Impeller is now the default on both iOS and Android in 2026. It doesn’t use native UI components — it draws everything itself. This means frame-perfect consistency across platforms and animations that don’t depend on a JavaScript thread.

React Native’s new architecture — JSI plus Fabric, stable and enabled by default since RN 0.74 — eliminated the old async bridge. Thread communication is now synchronous. This closed a significant performance gap in 2024-2025.

Side-by-side architecture comparison — Flutter's Dart VM plus Impeller rendering pipeline on the left, React Native's JSI plus Fabric architecture on the right, showing how UI updates flow from app code to the screen in each case

In practice, for this app, I couldn’t reliably tell the difference on a modern device. Both hit 60fps on the list scroll. The gap appears when you push harder — complex custom animations, heavy computation, very long lists. Flutter still wins there, but you have to be building something demanding to care.

Build sizes: Flutter release APK was 21MB. React Native bare was 18MB. Flutter carries its engine everywhere.

Ecosystem and Third-Party Libraries

React Native benefits from the entire JavaScript ecosystem. Need a date picker, a chart library, a PDF generator? There’s an npm package. The question is always whether it has real native bindings or is a pure-JS fallback.

Flutter’s pub.dev is smaller but more curated. Packages tend to be higher quality and better maintained because the community is more focused. For common needs — HTTP clients, SQLite, push notifications, state management — the Flutter ecosystem is solid.

Where Flutter still lags: some SDKs ship their React Native version first and Flutter second, sometimes months later. Analytics tools, some payment gateways, specific third-party integrations. If your app depends on one of these, check the Flutter SDK availability before committing.

For push notifications specifically, both flutter_local_notifications and Expo Notifications worked cleanly in my test. No meaningful difference in API quality. Flutter’s setup requires touching native config files directly; Expo abstracts that away.

What I Shipped

I shipped the Flutter version. The decision came down to factors specific to my situation.

UI consistency — our app has custom list animations and a design language that needs to look identical on iOS and Android. Flutter’s renderer guaranteed that without platform divergence.

Existing Dart exposure — my team had briefly used Flutter in 2023. The ramp-up cost was lower than switching to a mobile-specific React/TypeScript setup everyone would need to learn from scratch.

No exotic SDK requirements — we don’t depend on any third-party SDK that would put us at risk of the “Flutter SDK coming soon” problem.

Desktop is on the roadmap — Flutter’s single codebase across mobile, desktop, and web matters for us. We already run a Spring Boot backend; adding a Flutter desktop client for internal ops is straightforward.

If our team had been JS-heavy, or if we’d needed to share components with a web frontend, React Native would have been the right call.

The Honest Summary

Flutter makes more sense when:

  • Pixel-perfect custom UI — you control the renderer entirely
  • Performance-heavy animations — no JS thread bottleneck
  • Targeting beyond mobile — Flutter Desktop and Web are real options in 2026
  • Team is willing to learn Dart — the investment is smaller than it looks

React Native makes more sense when:

  • JS-heavy team — zero ramp-up if your team already knows React
  • Sharing logic with web — React Native Web and shared business logic are mature
  • Expo speed for MVPs — managed Expo is genuinely the fastest path to a working app
  • Broad SDK availability — some third-party SDKs still prioritize RN bindings

Neither framework is a mistake in 2026. The “Flutter vs React Native” war of 2019-2022 produced a lot of hot takes that aged poorly. Both ship real production apps. Both have active communities. Both have converged enough that your team’s background matters more than the framework’s raw capabilities.

The one thing I’d push back on: don’t pick React Native just because everyone on your team knows JavaScript. Mobile development has enough platform-specific quirks — permission flows, background tasks, notification entitlements, keychain storage — that the framework choice is secondary to understanding how iOS and Android actually work. That learning is required either way.

What are you running in mobile production in 2026? And if you switched frameworks at some point — Flutter to React Native or the reverse — what finally pushed you over the line?

Originally published on Medium.

JWT vs Session Tokens in Spring Boot: A Senior Dev’s Decision Guide

Side-by-side flow comparison — Session flow: client cookie to server to Redis lookup to SecurityContext; JWT flow: client Bearer token to server to local signature verify to SecurityContext. Annotate the revocation point on sessions (delete from Redis) and the revocation gap on JWT (no early invalidation without blocklist)

Three years ago I gave the same answer every time someone asked me about authentication in Spring Boot: “use JWT, it’s stateless, it scales.” I was half right and half wrong, and it took inheriting two production codebases — one broken in a very specific way — to understand which half was which.

This is not a tutorial on how to implement either one. It’s the decision guide I wish I’d had before I started recommending JWT by default.

What tutorials actually teach you

Most Spring Boot security tutorials walk you through JWT because it makes for a cleaner demo. You add a filter, validate a signature, set the SecurityContext, done. No database calls, no shared state, stateless by construction. It feels architecturally clean.

What they rarely show: what happens when a user changes their password. Or gets their account suspended. Or logs out on one device and expects that to mean something on all devices. With a pure JWT setup and no blocklist, the answer to all three is “nothing happens until the token expires.”

Sessions, on the other hand, feel old-fashioned. “That doesn’t scale.” “You need sticky sessions.” Neither of those is true anymore, and I’ll show you why.

How sessions actually work in Spring Boot

Spring Session with Redis is three annotations and a dependency:

@EnableRedisHttpSession
@Configuration
public class SessionConfig {
    // Spring Session handles everything else
}
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

The client gets an opaque session ID (typically 32 hex characters) stored in an HttpOnly; Secure cookie. Every request sends the cookie, Spring looks up the session in Redis, deserializes it, and populates the SecurityContext. The session data lives in Redis, not in the token itself.

Revocation is sessionRepository.deleteById(sessionId). Instant, no exceptions.

Horizontal scaling works out of the box — every instance connects to the same Redis. No sticky sessions needed. This is not 2009 anymore.

Side-by-side flow comparison — Session flow: client cookie to server to Redis lookup to SecurityContext; JWT flow: client Bearer token to server to local signature verify to SecurityContext. Annotate the revocation point on sessions (delete from Redis) and the revocation gap on JWT (no early invalidation without blocklist)

JWT: what you actually get

A JWT is a base64-encoded JSON payload (claims) signed with a secret or private key. The server does not store it. Verification happens locally by checking the signature — no database call, no network hop.

This matters in two specific situations:

Microservices that verify tokens independently. If you have five services and each needs to know who the caller is, sessions require every service to call a shared store or a central auth service. JWT lets each service verify the token locally with just the public key or shared secret.

Third-party and mobile clients. Browsers handle cookies automatically. Native apps and third-party API clients do not. JWT in the Authorization header works everywhere without cookie configuration.

Outside these two cases, you are paying the JWT costs without getting the JWT benefits.

The costs are real:

  • No revocation without a blocklist. A 15-minute access token cannot be invalidated early. A stolen token is valid until expiry. If you add a Redis blocklist to check on every request, you have just re-added the database call you were trying to avoid.
  • Token size. A session cookie is 32 bytes. A JWT with a handful of claims is 300–600 bytes in every request header, forever. In high-frequency internal APIs this adds up.
  • Implementation surface. JWT has a long history of security bugs: alg: none attacks, weak HMAC secrets, missing expiry validation, incorrect audience checks. Spring Security handles most of this correctly, but the complexity budget is higher than sessions.

The real decision framework

Stop asking “which is better?” and ask “what do I actually need?”

Use sessions when:

  • You control the frontend — a browser-based app using your own backend
  • You need immediate revocation: logout means logout, password change means all sessions die
  • You are building a monolith or a small service that owns its own auth
  • You are already running Redis for caching or queuing

Use JWT when:

  • Multiple independent services need to verify identity without calling a central store
  • You have non-browser clients — mobile apps, CLI tools, third-party integrations — that cannot easily handle cookies
  • You need federated identity: a token issued by an external IdP (Auth0, Keycloak, Cognito) that your service validates

Do not use JWT because:

  • “It’s stateless and scales” — sessions on Redis scale just as well across any number of instances
  • “Everyone uses it” — cargo-culting security decisions is how you end up with 7-day non-revocable tokens in production

The hybrid setup nobody talks about

Most production systems I’ve seen that do this well use a combination: sessions for the browser frontend, JWT for service-to-service calls and mobile clients.

Spring Security supports this cleanly with multiple SecurityFilterChain beans:

@Bean
@Order(2)
public SecurityFilterChain jwtFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/v1/**")
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .csrf(AbstractHttpConfigurer::disable)
        .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
    return http.build();
}

@Bean
@Order(1)
public SecurityFilterChain sessionFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/web/**")
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
        .formLogin(Customizer.withDefaults())
        .logout(logout -> logout.logoutSuccessUrl("/web/login?logout"))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/web/login").permitAll()
            .anyRequest().authenticated());
    return http.build();
}

Two chains, different matchers, different strategies. The browser app gets sessions and full revocation. API and service calls get JWT. Spring applies them in @Order sequence — the first matching chain wins.

What the performance difference actually looks like

Sessions add one Redis round-trip per request — typically 0.5–2ms on a well-configured local Redis, 2–5ms if Redis is in a separate availability zone. For a request that already takes 50–200ms to process, that is noise.

JWT validation is in-memory: parse the base64, verify the HMAC signature, check expiry. Sub-millisecond. If you are building an API that needs to handle thousands of requests per second with microsecond budgets, this difference matters. If you are building a standard web application or a business API, it does not.

The token size difference matters more than you would expect in aggregate. A session cookie is SESSION=<32-hex-chars> — about 50 bytes in the Cookie header. A JWT is Authorization: Bearer <base64> — typically 400–700 bytes in the Authorization header, on every single request. In an application with 10,000 active users making 20 requests per hour, that is roughly 70MB/hour in header overhead alone versus 5MB with sessions. On internal microservice APIs with high call frequency, this adds up to real cost.

Neither of these is a reason to choose one over the other by itself. They are factors to weigh against the architectural fit, not arguments that make the decision for you.

Security mistakes that are easy to make with JWT

Spring Security protects you from many JWT pitfalls if you use it correctly, but I have seen all of these in production codebases:

Symmetric secret too short. HS256 requires at least 256 bits (32 bytes). A short secret is brute-forceable. Generate it with openssl rand -base64 32 and store it in your secrets manager, not in application.yml.

No audience or issuer validation. If you have multiple services accepting the same JWT, a token issued for service A can be replayed against service B unless you validate the aud and iss claims. Spring Security’s JwtDecoder supports this with .claimValidator("aud", ...).

Logging the token. Access logs, debug statements, error traces. A JWT is a credential. Treat it like a password in your logging configuration.

Using RS256 in a monolith. RS256 (asymmetric) makes sense when multiple services need to verify tokens issued by a single auth service. In a monolith where only one service issues and verifies tokens, RS256 adds key management complexity with no security benefit. HS256 is the right default.

The mistake I see most often

Teams start with JWT because a tutorial recommended it. Six months later, they need to implement “logout from all devices” or “force re-authentication after a password change.” At that point they bolt on a Redis blocklist to invalidate tokens before expiry — which means every JWT validation now hits Redis on every request. They have kept all the JWT complexity and added the session store on top.

I have done this. The resulting code is harder to reason about than either pure sessions or pure JWT would have been.

If you need revocation, use sessions. If you genuinely need stateless cross-service verification, use JWT and design around the revocation limitation deliberately — 15-minute access tokens, refresh token rotation, a clear session invalidation policy documented before you ship — not as an afterthought six months later.

application.yml showing spring.session.store-type=redis, spring.session.timeout=30m, server.servlet.session.cookie.http-only=true, server.servlet.session.cookie.secure=true — the minimal Spring Session Redis config needed to replace an overly complex JWT setup

The boring answer is often the right one. Spring Session with Redis has been solving this problem correctly since 2015. JWT solves a specific distributed systems problem. Know which one you actually have before you choose.

What does your current setup use, and was it a deliberate choice or did it come from a tutorial?

Originally published on Medium.

What if Everybody Were Suddenly… Better?

Mario about to grab a mushroom and grow

We (and by we I often mean ‘me’ 🙃) spend LOTS of time talking about how to improve our work. LinkedIn-fluencers (even the crappy AI-bot ones) talk about optimizing and improving and making things better all the time. It’s great to have goals like this: self-improvement is admirable, and I fully believe that the best people at any given task (software engineering, plumbing, music, you name it) are the ones who are always working to improve what they do.

To absolutely no one’s surprise, though, we don’t often see the organizational improvements we advocate for (or maybe some of us are surprised by this? idk, let me know in the comments!). Working with people is a messy business: sometimes they’re politically or financially incentivized to follow “The Old Way”, and no amount of logic will change their minds. Sometimes we do a crappy job of stating our case and they don’t understand the change we want to make. Sometimes “this is suboptimal, but I don’t have to put in additional effort” wins out over a better idea that requires a little sweat equity. We might give the needle a small nudge, but often we put in a lot of effort and glean relatively little progress from it. It can be maddening at times.

Fever Dream Fantasy: The Thought Experiment

There’s an amazing dude named John Cutler over at DotWork who, at the end of 2025, proposed a really intriguing thought experiment. As I’m drafting this almost two full months later, I’ve had some time to think about what he suggested for a while now…

Here’s a link to his original post, but if for some reason LinkedIn decides to eat that, I’ve copied the text below for posterity:

Here’s a fun little thought exercise. Suddenly everyone in your company is 30% more capable, more competent, more skilled, more experienced.

What would actually happen?

Mario about to grab a mushroom and grow

That kind of outcome feels too good to be true. (Which is probably why it’s a thought experiment!) I mean, imagine showing up on Monday morning and EVERYONE IN THE WHOLE COMPANY is leveled up like that! Wouldn’t it be great? We’d be SO profitable and SO efficient and we’d absolutely CRUSH it every day, wouldn’t we?

Padme looking hopeful
…wouldn’t we?

Padme looking worried

The Answer Lies in What DIDN’T Improve

Our 30% improvements were in:

  • Capability
  • Competency
  • Skill
  • Experience

But think about what isn’t in that list:

  • Incentives
  • Priorities
  • Decision-making latency
  • Trust levels
  • Clarity of ownership
  • How work flows
  • How conflict is handled
  • How failure is punished

Oh, sure, you’ve increased the Talent. But you’ve left them in a broken system. Think about it like this: your engineer is 30% better at shipping things, but if you still have problems with prioritization, they’re shipping the wrong thing 30% faster.

Did you really gain anything by that?

What if your approval process takes weeks, or even months? Making the engineers work faster doesn’t really help. (BTW: This is a concept codified forty years ago, called the Theory of Constraints. Nothing new!)

What if your team struggles with psychological safety? Even if you increase their skill overnight… they’re still not going to contribute more when it isn’t safe to speak up.

Leadership Always Falls for The “Get Better People” Trap

Listen to senior leadership discussions, and you’ll realize that many (most? maybe all?) have fallen for it:

  • “I only want A players on my team.”
  • “We need more Senior/Staff/Principal Engineers.”
  • “We need higher standards in the organization.”
  • “We’re going to outsource {some competency} because it will be more efficient than investing in our teams developing the skill.”

It’s tantalizing to think this way:

“Oh, well Bob isn’t working out for us – he’s competent, but he’s just too slow. Let’s cut him and hire one of those 10x guys I keep hearing about.”

This kind of thinking feels natural to us. If you grew up playing TECMO Bowl, you remember Bo Jackson. He was that 10x Running Back that you and your siblings always fought over – because whoever had him was going to win.

Touchdown Bo Jackson

On the surface, managers and directors think like this because it feels like meritocracy. “We gave Bob a chance, he couldn’t hack it here.” It’s tangible – something they can see and do, so they feel like they’re hands-on “managing” their team. But I think they’re actively destroying their team by doing this.

Wanna know why?

  • The revolving door “Bob couldn’t hack it” approach is easier than doing the culture work that fixes what actually kept Bob from succeeding.
  • Bob gets the blame that rightly belongs to the leadership system… and every other engineer sees that management will cut people instead of admitting a fault.

Here are some “translations” you might want to consider:

Easy Story Hard Reality
We need A-players. Our environment suppresses B+ players instead of helping them grow.
We need more Seniors. Our decision process is chaotic.
We need higher hiring standards. We don’t protect focus, or prioritize well, or communicate well.

For Senior Leadership, it can be psychologically safer to blame their people than to honestly assess the management system.

Environment Shapes Your Team’s Productivity

If you’ve ever read anything on the Adventures of Blink, you know I firmly believe that good Developer Experience is the foundation of your engineering team’s ability to increase revenue… but judging from what I observe around me, there aren’t many in senior leadership roles who’ve made that connection.

Let’s consider it like a simple equation:

Impact = Talent × Environment

So if everyone were 30% more talented, 1.3x talent seems pretty exciting, doesn’t it?

Set your environment to 0.5, and even your extra-talented team underdelivers.

Now let’s look at it a different way: If you provide a great environment, even your average talent looks incredible!

Why we can trust the equation

Blink, you just made that formula up. Well… you’re right about that… but I think it works anyway! Here’s why:

Clear priorities → Fewer wasted cycles

Strong trust → Faster decisions

Tight feedback loops → Rapid improvement

Psychological safety → Better idea-sharing

Good DevEx → Skill converts to output

Providing a safe, empowering, collaborative environment clearly acts as a force multiplier across all these dimensions.

The Right Questions

The easy question for leadership is “How do we get better people?” It’s an alluring question because people in management fantasize that the answer is found in hiring practices, in clever interview questions, in asserting themselves as “high-performance leaders” who go and get things done.

But the right question to ask looks a little different:

What would need to change for our current people to operate at 130%?

That’s a tough pill to swallow because it reframes the responsibility.

In order to answer that question, you can start by looking for the answers to these:

Where do good ideas go to die?

You might actually have lots of great ideas coming from your staff… but they get strangled before they’re realized. Find out why that is, and fix it.

Where does energy leak?

I’ve watched a highly motivated team absolutely burn out because they spent as much time fighting the system as they did innovating.

Where does friction accumulate?

You didn’t wake up one morning and say “hey let’s implement a painful cumbersome process” (well, at least I hope you didn’t!). It’s been compounding in the shadows for a while. Ask the team where it is, and listen carefully. They’ll show you.

What behaviors are we quietly (and/or accidentally) rewarding?

I once knew a manager who obsessed over his team’s compliance scores. If a finding came out for his team, he insisted on dropping ALL planned work to deal with it.

Governance and Compliance learned that if they needed something from him, they could rattle the Findings saber and instantly move to the front of the line.

His team learned that their innovation wasn’t as important as their Compliance scores.

Think about how that affected everyone’s choices.

Not to put too fine a point on it, but…

If 30% more capable people wouldn’t radically transform your company, that’s not a talent problem; it’s a system problem.

The fantasy is that better people will fix the system.
The reality is that the system shapes what people can become.

Wrapping Up

I get it: managers want to manager. They see a quick path to “fixing the problems” and it tempts them with their own agency:

“You can fix this broken team, just get rid of the low performers and get some rockstars in there”.

I’ve felt like that before. And it’s hard to tell the difference between a B+ player who’s been hamstrung by a bad environment and a C- player that’s genuinely a bad fit. All I’m saying is that maybe we need that trigger-finger to be a little less itchy, and apply a little introspection first.

En İyi Araba Ekran-Multimedya Takan Yerler -2026

Aracına multimedya ekranı, geri görüş kamerası veya Android tabanlı multimedya sistemi taktırmak isteyenler için 2026 yılının en iyi oto multimedya firmalarını listeledik. Kaliteli montaj hizmeti sunan ve sürücüler tarafından tercih edilen işletmeleri bu rehberde bulabilirsiniz.

OCI Web Application Firewall (WAF) Deep Dive: Architecture, Traffic Inspection, Threat Protection, and Enterprise Security Design

Introduction

Modern applications exposed to the internet constantly face threats including SQL injection, bots, DDoS attacks, malicious traffic, and Layer 7 attacks.

OCI Web Application Firewall (WAF) helps protect internet-facing applications by inspecting and filtering HTTP/HTTPS traffic before it reaches backend applications.

What is OCI WAF?

OCI WAF performs Layer 7 traffic inspection, request filtering, threat detection, and policy enforcement before traffic reaches protected applications.

-> It acts like a security checkpoint for web traffic.

Why WAF Is Needed

✔ SQL Injection
✔ Cross-Site Scripting (XSS)
✔ Bot Traffic
✔ DDoS attacks
✔ Credential stuffing
✔ Malicious HTTP requests

Real-world Example

Attacker sends malicious SQL payload
→ Application vulnerable
→ Database compromise possible

WAF helps block malicious requests before they reach applications.

OCI WAF Architecture

Users
│
▼
OCI WAF
│
Traffic Inspection
│
▼
OCI Load Balancer
│
▼
Application Servers

Understanding Layer 7 Security

Traditional firewalls focus mainly on network traffic.
WAF focuses on HTTP/HTTPS application traffic.

-> OCI WAF operates at Layer 7 of the OSI model.

Layer 7 Examples

✔ URLs
✔ HTTP headers
✔ Cookies
✔ Request payloads
✔ API requests
✔ User agents

Request Inspection Workflow

Traffic Flow

  1. User sends HTTPS request
  2. Request reaches OCI WAF
  3. WAF evaluates rules
  4. Threat intelligence checks executed
  5. Malicious payload detected
  6. Request blocked or allowed
  7. Safe traffic forwarded

OCI WAF Security Policies

WAF policies define how traffic should be inspected and filtered.

Examples
✔ Access rules
✔ IP blocking
✔ Country filtering
✔ Threat protection
✔ Rate limiting
✔ Bot management
✔ CAPTCHA challenges

Rate Limiting

Rate limiting controls excessive traffic requests.

Real-world example:

Bot sends 10,000 login attempts
→ WAF rate limiting blocks abuse

Bot Protection

Not all traffic comes from real users.

Bot Examples
✔ Credential stuffing bots
✔ Scraping bots
✔ Fake traffic generators
✔ Automated attack tools

-> OCI WAF helps differentiate legitimate traffic from automated malicious behavior.

OCI WAF Deployment Models

OCI WAF can be deployed in different architectures depending on traffic flow, security requirements, and application design.

Edge WAF

Traffic inspected closer to internet edge locations before reaching OCI infrastructure.

Regional WAF

Traffic inspection occurs within OCI regional deployment architecture.

Load Balancer Attached WAF

OCI WAF integrated directly with OCI Load Balancer for backend application protection.

WAF + Load Balancer Integration

Users
│
▼
OCI WAF
│
▼
OCI Load Balancer
│
▼
Backend Applications

WAF protects applications before traffic reaches backend infrastructure.

Real Enterprise Scenario

Example:

E-Commerce Platform

Users
→ WAF
→ Load Balancer
→ Web Servers
→ Payment Application
→ Database

Attack Example

Attacker sends malicious login traffic
→ WAF blocks suspicious requests
→ backend remains protected

Common WAF Challenges

✔ False positives
✔ Legitimate traffic blocked
✔ Poor rule tuning
✔ SSL misconfiguration
✔ Missing exclusions
✔ Excessively strict policies

Understanding False Positives

Sometimes legitimate application traffic may match security rules and become blocked accidentally.

Examples :

✔ Complex API payloads
✔ Encoded requests
✔ Search queries with special characters
✔ Custom application parameters

-> Effective WAF deployment requires balancing security and application usability.

WAF vs Traditional Firewall

Traditional Firewall WAF
Network traffic HTTP/HTTPS traffic
IP/Port filtering Application inspection
Layer 3/4 Layer 7
Infrastructure protection Application protection

Observability & Monitoring

WAF visibility is critical for security operations.

Monitoring Areas

✔ Blocked requests
✔ Attack patterns
✔ Bot traffic
✔ Request trends
✔ Security events
✔ Rate limit violations

Enterprise Best Practices

✔ Enable HTTPS inspection
✔ Continuously tune rules
✔ Monitor false positives
✔ Combine WAF with Load Balancer
✔ Enable logging
✔ Review attack trends
✔ Test security policies regularly

Understanding WAF Limitations

Although OCI WAF provides strong Layer 7 protection, it is not a complete replacement for secure application design.

✔ HTTP/HTTPS traffic only
✔ Requires rule tuning
✔ Cannot fully stop business logic abuse
✔ Secure coding still required
✔ Advanced attacks may bypass weak policies

Defense in Depth Security Architecture

Enterprise security should combine multiple security layers rather than relying on a single protection mechanism.

✔ OCI WAF
✔ OCI Network Firewall
✔ NSGs
✔ IAM Policies
✔ Secure Coding
✔ Vulnerability Scanning
✔ Logging & Monitoring
✔ Threat Detection

Conclusion

OCI WAF provides Layer 7 application protection by inspecting, filtering, and securing HTTP/HTTPS traffic before it reaches backend applications.

By combining threat protection, rate limiting, bot mitigation, and traffic inspection, OCI WAF helps organizations improve application security and operational resilience in modern cloud environments.

Selling Digital Products in a Country PayPal Refuses to Touch

The Problem We Were Actually Solving

At first glance, the issue seemed to be about finding a suitable payment gateway that supported our business model. But the more I dug, the more I realized that the problem was deeper. Our e-learning platform was designed to be a global business, yet our payment infrastructure was tied to Western-centric platforms like PayPal and Stripe. These platforms didn’t provide a straightforward way for us to onboard customers from a country they deemed “high-risk.”

What We Tried First (And Why It Failed)

We tried to work around the issue by using alternative payment methods, such as bank transfers and cryptocurrency. However, these options introduced significant friction for our customers, and our sales numbers plummeted. We also considered creating a separate business entity in a country with fewer restrictions, but this approach would have required significant overhead and legal expenses.

The Architecture Decision

After months of research and experimentation, we decided to implement a custom payment gateway that leveraged local payment methods popular in our home country. We partnered with a regional payment processor that offered a seamless checkout experience for our customers. This decision required significant upfront investment, but it ultimately allowed us to bypass the platform restrictions imposed by PayPal, Stripe, Gumroad, and Payhip.

What The Numbers Said After

The results were eye-opening. By integrating a custom payment gateway, we reduced our payment processing fees by 40% and increased our conversion rates by 25%. Our customers were able to complete purchases with ease, and our sales numbers began to recover. The new infrastructure also gave us a competitive edge in the regional e-learning market, where many businesses struggled with similar payment restrictions.

What I Would Do Differently

In hindsight, I would have invested in a custom payment gateway much earlier in our platform’s development. I would have also explored more regional payment processors and alternative payment methods that could have helped us bypass platform restrictions. However, the key takeaway is that platform restrictions are not a reason to abandon your business goals. With the right architecture and engineering decisions, you can overcome even the most seemingly insurmountable obstacles.

PostgreSQL backup tool Databasus released backup verification in real database Docker containers

Backup restore verification

Databasus just shipped restore verification. The idea behind it is short. A backup job that ends with “success” only proves the dump command ran. It does not prove you can restore the file it produced. Restore verification closes that gap. It takes your latest backup, restores it into a throwaway database container and checks that the data actually came back.

Backup restore verification

Databasus is the most widely used tool for PostgreSQL backup, and it already handles logical, physical and point-in-time recovery backups. Restore verification is the piece that now confirms those backups will actually restore. You can read the full feature docs on the restore verification page. The rest of this article covers what it does, how the report reads and how to switch it on.

A backup that finished is not a backup you can restore

Here is the uncomfortable part of running backups. Most setups check two things and call it done. They check that the backup job did not throw an error, and sometimes they check a file checksum. Both checks are real and worth having. The trouble is that neither one tells you the backup will restore.

A dump can exit with code 0 and still be missing data. A role without read permission on certain tables can make the dump tool skip those objects quietly. So can a missing extension on the source or a tablespace mismatch. None of that shows up in an exit code, and a checksum will happily confirm that an incomplete dump is a perfectly intact file.

Signal What it proves What it still misses
Backup job succeeded The dump command ran and exited without an error Objects skipped because a role lacked read permission
Archive checksum matches The stored file is intact, with no bit rot Whether the dump is complete or can be restored
Restore verification passed The backup restored into a live database and the rows are there Nothing meaningful, since it runs the actual restore

A green backup status and a matching checksum are both fine to have. They just leave one question open, and it is the question that matters most. Restore verification is the check that answers it directly.

What restore verification does

When a verification runs, it does not parse the backup or simulate a restore. It performs a real one. The sequence is the same every time, and it is easy to follow from start to finish.

  • Pulls the most recent successful backup for the database
  • Spins up a fresh database container and restores the archive into it with the engine’s native restore tool
  • Counts the rows in every table and checks the restored size against the backup
  • Destroys the container along with everything inside it
  • Sends the result back to Databasus

Each run is self-contained. It never touches your production database, and it leaves nothing behind once the check is done.

Inside a real database container

The restore does not run against your live server. Databasus starts a separate Docker container for the job, on the host where the verification agent runs. That container runs the matching database engine, so the restore uses the real tooling instead of an approximation of it.

Running it this way has a couple of practical upsides. The check cannot interfere with production traffic, because it never connects to the production server at all. And since the container is disposable, a verification can restore a full database without you hunting for spare space on a real instance. When the run finishes the container and its data are deleted, so a week of daily checks will not slowly fill a disk.

This is the part the feature name points at. Your backup gets proven against a real engine in a real container, and then that container is thrown away.

Reading a verification report

Every verification leaves a report behind. At a glance you get a status, which is enough to see whether anything needs attention. Open the run itself and you get the detail, including exactly what came back from the archive.

Field What it shows
Status One of Pending, Running, Successful, Failed or Canceled
Timeline Every stage of the run with its own timestamp
Restore exit code The exit code returned by the database’s native restore tool
Restored database size The size of the restored database, checked against the backup
Schema and table counts How many schemas and tables came back from the archive
Per-table row counts A row count for each table in the restored database
Failure message Shown at the top of a failed run, so the reason is the first thing you see

The per-table row counts are the part worth watching. If a table you expect to be large comes back empty, the report shows it now, long before the day you actually need that backup to restore.

Turning it on with a verification agent

Restore verification is heavier than a normal backup job. It downloads a full backup, starts a database and runs a restore. Work like that does not belong inside the main Databasus container, so it runs on a small separate worker called the verification agent.

The agent is a single lightweight Go binary. You create it in the Databasus UI first, which gives you something to register against, then you launch the binary on a host you control. It dials out to Databasus, picks up verification jobs from a queue and reports the results back. You can run it next to Databasus or on a different machine, whichever suits your hardware.

Once it is running and registered, the agent appears in Databasus and starts taking jobs.

What the agent host needs

The agent does not ask for much, but it does have a few hard requirements. Most of them follow from the fact that it runs real databases in Docker. It is worth checking these before you launch it.

  • Outbound HTTPS access to your Databasus URL, since the agent dials out and is never exposed
  • Docker installed and running on the host
  • Free disk of roughly twice your largest backup, with at least 1 GB of headroom
  • At least 1 CPU core and 512 MB of RAM for every verification running at the same time

If a host already runs Docker and has some disk to spare, it can almost certainly run an agent.

Setting a resource budget

You will not want a verification to consume the whole machine, so the agent runs inside a budget. You pass four flags when you launch it, and Databasus divides that budget across the jobs it sends to the agent.

Flag What it limits Example
--max-cpu CPU cores the agent may use 2
--max-ram-mb RAM in megabytes the agent may use 2048
--max-disk-gb Disk in gigabytes available for restores 20
--max-concurrent-jobs How many verifications run at the same time 1

There is a floor of 1 CPU core and 512 MB of RAM per job. If your budget cannot cover that floor for the concurrency you asked for, the agent does not fail. It simply advertises a lower number of concurrent jobs and works within what it has. Set the budget once at launch and the agent stays inside it.

Schedules, the queue and notifications

You decide how often each database gets verified. There is no single right cadence, so Databasus gives you three ways to trigger a run, and you pick whichever fits the database in front of you.

  • After every successful backup, the moment it finishes
  • On an hourly, daily, weekly or monthly cadence at a time you choose
  • On a custom UTC cron expression for anything the presets miss, for example 0 4 * * 0 for every Sunday at 04:00

The “after every backup” option raises a fair question. What if backups arrive faster than verifications can finish? Databasus handles that by cancelling any pending verification for a database as soon as a fresher backup shows up. Only the most recent backup waits in line, so checks never pile up and you always verify the newest data.

You can also run a one-off verification by hand. Each database has a “Restore verifications” tab, and from there you can start a single check without changing the schedule. It is handy for spot-checking one particular backup, or for testing a new agent right after you set it up.

Results can go to any notifier already wired to the database, so Slack, Discord, Telegram, email and the rest all work without extra setup. There are two separate toggles, one for verification success and one for verification failure, and they are independent of each other. Most teams switch on the failure one only, since a constant stream of “it worked” messages tends to get ignored after a while.

Set a trigger once, point it at the notifiers you already use and the database keeps verifying itself from there.

Conclusion

Restore verification changes what a backup status means. Instead of “the dump finished”, you get “the backup restored into a real database, and here are the row counts”. That is the difference between hoping a backup works and knowing it does.

Databasus is a free, open source and self-hosted backup tool, and it has become an industry standard for PostgreSQL backups. It is Apache 2.0 licensed, with more than 600,000 Docker pulls, around 7,000 GitHub stars and over 30 contributors. Restore verification ships in the same open source build as everything else, with no paid tier and no feature gate, and the code is all on GitHub.

If you already run Databasus, set up an agent and turn verification on for your most important database first. If you do not run it yet, this is one of those features worth having in place well before the day you actually need a backup to come back.