Email Authentication Dkim Spf Dmarc – Updated 2026 Guide

Bounce Rate Reduction: The Complete Technical Guide for Email Engineers

Bounce rate is the silent killer of email deliverability. A rate above 2% gets you flagged. Above 5% gets you blocked. Most teams don’t know they have a problem until their IP reputation is already damaged.

This guide covers every technical lever you can pull to keep bounces below 1%.

Hard Bounces vs Soft Bounces: What’s Actually Happening

A hard bounce means the recipient address doesn’t exist — permanently. This is poison for your sender reputation and must be handled within 24 hours.

A soft bounce is temporary — mailbox full, server busy, message too large. These accumulate too. A pattern of soft bounces on the same address signals a problem.

Rule: Hard bounces = immediate list removal. Soft bounces = 3 attempts over 5 days, then quarantine.

List Hygiene: The Foundation of Low Bounce Rates

Validate at Point of Capture

Never add an address to your list without first confirming it exists:

SMTP Dialog Check:
$ telnet mx.example.com 25
220 mx.example.com ESMTP
HELO yourserver.com
250 mx.example.com Hello yourserver.com
MAIL FROM:<[email protected]>
250 OK
RCPT TO:<[email protected]>
250 OK  ← valid

If you get 550 User unknown or 550 5.1.1 back, the address is dead.

Tools: smtp-user-enum, direct SMTP verification, or API services like ZeroBounce and AbstractAPI.

Re-validate Your Existing List Monthly

Your list decays ~22% per year even without adding new addresses. Run monthly hygiene checks:

  1. Flag addresses with no opens/clicks in 90 days for re-engagement
  2. Re-validate dead addresses via SMTP check before next send
  3. Remove anything that bounces twice consecutively

Role-Based Addresses Are Death

admin@, info@, postmaster@, sales@, support@ — these addresses are monitored by bots, often catch-all, and always high-bounce. Don’t send to them unless you verified them specifically.

SPF, DKIM, and DMARC: Your Deliverability Shield

Misconfigured authentication is the #1 cause of unexpected bounces. Recipients’ servers reject unauthenticated mail before it even reaches the inbox folder.

SPF (Sender Policy Framework)

SPF tells receiving servers which mail servers are allowed to send for your domain:

v=spf1 include:_spf.yourmailprovider.com ~all

Too many mechanisms (+mx, +a, +ip4 pointing to your web server) creates SPF lookup failures. Hard limit: 10 DNS lookups per SPF record. Use include: chains wisely.

DKIM (DomainKeys Identified Mail)

DKIM attaches an encrypted signature to every message. The receiving server decrypts it using your public key published in DNS. If the signature doesn’t match, the message is rejected.

KumoMTA DKIM setup:

dkim {
  sign = true
  selector = "mail"
  domain = "postmta.com"
  private_key = "/etc/kumomta/dkim/private.pem"
}

Rotate keys every 90 days. Compromised DKIM = spoofed mail using your domain.

DMARC (Domain-based Message Authentication, Reporting & Conformance)

DMARC ties SPF and DKIM together with a policy:

v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100

Start with p=none (monitor only) for 2 weeks, read your DMARC reports, then move to p=quarantine and finally p=reject once you’ve confirmed everything authenticates correctly.

List Segmentation: Send to the Right Addresses

Sending the same message to your entire list is the fastest way to inflate your bounce rate. Different segments have different risk profiles:

High-risk segments (bounce rate 2-5%):

  • Addresses added more than 18 months ago with no recent open activity
  • Addresses collected via purchased or rented lists
  • B2B addresses at companies with high employee turnover

Low-risk segments (bounce rate < 0.5%):

  • Double opt-in subscribers
  • Addresses with opens/clicks in the last 30 days
  • Customer addresses (already verified at signup)

Golden rule: Always send new campaigns to your lowest-risk segment first. If you get >1% bounces, something is wrong — stop and investigate.

Sending Volume: The Velocity Trap

Bouncing more than 5% of a batch in a single session is a red flag, regardless of your overall list health. Receiving servers track bounce velocity, not just rate.

Safe sending velocity:

  • New IP: Start at 50-100 emails/hour for first 48 hours
  • Warming up: Double volume every 2-3 days IF bounce rate stays below 1%
  • Established IP: Cap daily growth at 20% until you reach target volume

KumoMTA rate limiting:

remote_queue "outbound" {
  concurrency = 200
  rate = "5000/hour"
}

Feedback Loops: Know Before They Bounce

Most major mailbox providers (Gmail, Microsoft, Yahoo) support Feedback Loop (FBL) notifications. When a recipient marks your mail as spam, you get an abuse complaint and can immediately suppress that address.

Register for FBL:

  • Gmail/Google Workspace: Postmaster Tools (free, no signup needed — just publish DKIM/SPF correctly)
  • Microsoft SNDS: https://sendersupport.office.com/ — register your IPs
  • Yahoo: Requires contact with Yahoo’s feedback loop service

KumoMTA FBL setup:

feedback_loop {
  enabled = true
  address = "[email protected]"
  auth_id = "your_auth_id"
}

Monitoring: Set Up Bounce Alerts

Don’t wait for your sender score to drop to discover you have a bounce problem. Set thresholds that trigger immediate action:

Metric Warning Critical
Hard bounce rate > 0.5% > 1%
Soft bounce rate > 3% > 5%
Unknown user rate > 1% > 2%

Create a monitoring dashboard tracking:

  • Bounce rate by campaign
  • Bounce rate by IP
  • Bounce rate by sending domain
  • Complaint rate (FBL)
  • IP reputation score (Sender Score, Google Postmaster)

The Bounce Recovery Checklist

If your IP is already damaged:

  1. Stop sending immediately — every bounce while warming further damages reputation
  2. Identify the bad list segment — is it a specific acquisition channel?
  3. Clean your list — remove all hard bounces, 6+ month inactive addresses
  4. Move to a new IP — your old IP is likely blacklisted
  5. Warm the new IP slowly — 48 hours at 50 emails/hour, then gradual ramp
  6. Segment aggressively — only send to verified, engaged addresses for first 30 days
  7. Monitor your reputation — Sender Score, Postmaster Tools, SNDS

Full reputation recovery takes 3-6 months of consistent clean sending.

Tools for Bounce Rate Management

  • ZeroBounce — Email verification API with 99% accuracy
  • AbstractAPI Email Validator — Bulk validation with spam trap detection
  • NeverBounce — Real-time validation with guaranteed accuracy SLA
  • MXToolbox — DNS and blacklist monitoring
  • Google Postmaster Tools — Free reputation data for Gmail recipients
  • Microsoft SNDS — Free IP reputation data for Outlook/Hotmail

Bottom line: Bounce rate is a list quality problem, not a sending problem. Every percentage point above 2% is lost revenue and a damaged sender reputation that takes months to rebuild.

This guide is part of the PostMTA Email Infrastructure Series. For SMTP relay configuration and MTA setup guides, see KumoMTA Setup Guide.

Bounce Rate Reduction Guide – Updated 2026 Guide

Bounce Rate Reduction: The Complete Technical Guide for Email Engineers

Bounce rate is the silent killer of email deliverability. A rate above 2% gets you flagged. Above 5% gets you blocked. Most teams don’t know they have a problem until their IP reputation is already damaged.

This guide covers every technical lever you can pull to keep bounces below 1%.

Hard Bounces vs Soft Bounces: What’s Actually Happening

A hard bounce means the recipient address doesn’t exist — permanently. This is poison for your sender reputation and must be handled within 24 hours.

A soft bounce is temporary — mailbox full, server busy, message too large. These accumulate too. A pattern of soft bounces on the same address signals a problem.

Rule: Hard bounces = immediate list removal. Soft bounces = 3 attempts over 5 days, then quarantine.

List Hygiene: The Foundation of Low Bounce Rates

Validate at Point of Capture

Never add an address to your list without first confirming it exists:

SMTP Dialog Check:
$ telnet mx.example.com 25
220 mx.example.com ESMTP
HELO yourserver.com
250 mx.example.com Hello yourserver.com
MAIL FROM:<[email protected]>
250 OK
RCPT TO:<[email protected]>
250 OK  ← valid

If you get 550 User unknown or 550 5.1.1 back, the address is dead.

Tools: smtp-user-enum, direct SMTP verification, or API services like ZeroBounce and AbstractAPI.

Re-validate Your Existing List Monthly

Your list decays ~22% per year even without adding new addresses. Run monthly hygiene checks:

  1. Flag addresses with no opens/clicks in 90 days for re-engagement
  2. Re-validate dead addresses via SMTP check before next send
  3. Remove anything that bounces twice consecutively

Role-Based Addresses Are Death

admin@, info@, postmaster@, sales@, support@ — these addresses are monitored by bots, often catch-all, and always high-bounce. Don’t send to them unless you verified them specifically.

SPF, DKIM, and DMARC: Your Deliverability Shield

Misconfigured authentication is the #1 cause of unexpected bounces. Recipients’ servers reject unauthenticated mail before it even reaches the inbox folder.

SPF (Sender Policy Framework)

SPF tells receiving servers which mail servers are allowed to send for your domain:

v=spf1 include:_spf.yourmailprovider.com ~all

Too many mechanisms (+mx, +a, +ip4 pointing to your web server) creates SPF lookup failures. Hard limit: 10 DNS lookups per SPF record. Use include: chains wisely.

DKIM (DomainKeys Identified Mail)

DKIM attaches an encrypted signature to every message. The receiving server decrypts it using your public key published in DNS. If the signature doesn’t match, the message is rejected.

KumoMTA DKIM setup:

dkim {
  sign = true
  selector = "mail"
  domain = "postmta.com"
  private_key = "/etc/kumomta/dkim/private.pem"
}

Rotate keys every 90 days. Compromised DKIM = spoofed mail using your domain.

DMARC (Domain-based Message Authentication, Reporting & Conformance)

DMARC ties SPF and DKIM together with a policy:

v=DMARC1; p=quarantine; rua=mailto:[email protected]; pct=100

Start with p=none (monitor only) for 2 weeks, read your DMARC reports, then move to p=quarantine and finally p=reject once you’ve confirmed everything authenticates correctly.

List Segmentation: Send to the Right Addresses

Sending the same message to your entire list is the fastest way to inflate your bounce rate. Different segments have different risk profiles:

High-risk segments (bounce rate 2-5%):

  • Addresses added more than 18 months ago with no recent open activity
  • Addresses collected via purchased or rented lists
  • B2B addresses at companies with high employee turnover

Low-risk segments (bounce rate < 0.5%):

  • Double opt-in subscribers
  • Addresses with opens/clicks in the last 30 days
  • Customer addresses (already verified at signup)

Golden rule: Always send new campaigns to your lowest-risk segment first. If you get >1% bounces, something is wrong — stop and investigate.

Sending Volume: The Velocity Trap

Bouncing more than 5% of a batch in a single session is a red flag, regardless of your overall list health. Receiving servers track bounce velocity, not just rate.

Safe sending velocity:

  • New IP: Start at 50-100 emails/hour for first 48 hours
  • Warming up: Double volume every 2-3 days IF bounce rate stays below 1%
  • Established IP: Cap daily growth at 20% until you reach target volume

KumoMTA rate limiting:

remote_queue "outbound" {
  concurrency = 200
  rate = "5000/hour"
}

Feedback Loops: Know Before They Bounce

Most major mailbox providers (Gmail, Microsoft, Yahoo) support Feedback Loop (FBL) notifications. When a recipient marks your mail as spam, you get an abuse complaint and can immediately suppress that address.

Register for FBL:

  • Gmail/Google Workspace: Postmaster Tools (free, no signup needed — just publish DKIM/SPF correctly)
  • Microsoft SNDS: https://sendersupport.office.com/ — register your IPs
  • Yahoo: Requires contact with Yahoo’s feedback loop service

KumoMTA FBL setup:

feedback_loop {
  enabled = true
  address = "[email protected]"
  auth_id = "your_auth_id"
}

Monitoring: Set Up Bounce Alerts

Don’t wait for your sender score to drop to discover you have a bounce problem. Set thresholds that trigger immediate action:

Metric Warning Critical
Hard bounce rate > 0.5% > 1%
Soft bounce rate > 3% > 5%
Unknown user rate > 1% > 2%

Create a monitoring dashboard tracking:

  • Bounce rate by campaign
  • Bounce rate by IP
  • Bounce rate by sending domain
  • Complaint rate (FBL)
  • IP reputation score (Sender Score, Google Postmaster)

The Bounce Recovery Checklist

If your IP is already damaged:

  1. Stop sending immediately — every bounce while warming further damages reputation
  2. Identify the bad list segment — is it a specific acquisition channel?
  3. Clean your list — remove all hard bounces, 6+ month inactive addresses
  4. Move to a new IP — your old IP is likely blacklisted
  5. Warm the new IP slowly — 48 hours at 50 emails/hour, then gradual ramp
  6. Segment aggressively — only send to verified, engaged addresses for first 30 days
  7. Monitor your reputation — Sender Score, Postmaster Tools, SNDS

Full reputation recovery takes 3-6 months of consistent clean sending.

Tools for Bounce Rate Management

  • ZeroBounce — Email verification API with 99% accuracy
  • AbstractAPI Email Validator — Bulk validation with spam trap detection
  • NeverBounce — Real-time validation with guaranteed accuracy SLA
  • MXToolbox — DNS and blacklist monitoring
  • Google Postmaster Tools — Free reputation data for Gmail recipients
  • Microsoft SNDS — Free IP reputation data for Outlook/Hotmail

Bottom line: Bounce rate is a list quality problem, not a sending problem. Every percentage point above 2% is lost revenue and a damaged sender reputation that takes months to rebuild.

This guide is part of the PostMTA Email Infrastructure Series. For SMTP relay configuration and MTA setup guides, see KumoMTA Setup Guide.

I’ve Changed My Opinion On Vibe Coding

I used to be pretty skeptical of vibe coding. Watching people ship apps without understanding a single line of what was generated always felt like a disaster waiting to happen, especially when beginners hit the first real bug and have no idea where to start. And the idea that you can build successful software or SaaS without learning software development? I never bought that for a second.

That said, my opinion has shifted a bit. The models are better, I’ve been using Claude Code, GPT-5.5, and OpenClaw in a much more serious way, and I’ve seen firsthand where AI can actually carry real weight. The catch is that it only works when you already understand what you’re doing.

Vibe coding is the extreme end of the AI coding spectrum

There’s a real spectrum when it comes to coding with AI. On one end, you have one-shot prompting with platforms like Lovable. On the other, you have autocomplete in VS Code. Somewhere in the middle is the sweet spot: let the agent write the code, but you still make the architectural decisions, write the specs, and test the result.

That middle ground is what I actually teach in my AI course, and it’s a lot different from what I mean by vibe coding.

Vibe coding, to me, is when you’re barely looking at the code at all. You’re going off the vibes instead of the syntax, and for a long time I was totally against that. I still think a lot of people confuse “using AI” with “understanding software,” and those are not the same thing.

Better models changed the equation, not the rules

A big part of why I’ve softened is simple: the models are just better now. Claude Opus 4.7 with Claude Code, GPT-5.5 with Codex, and even OpenClaw with GPT-5.5 are all good enough that, if you know how to direct them, they can do a lot of the heavy lifting.

I’m seeing less hallucination, more first-try success, and a lot less need to babysit every line the way I would have with something like GPT-5.3. That matters.

But the model getting better is only half the story. The other half is that I’ve learned how to work with these systems more effectively. I know how to manage context and memory, how to map out documentation and spec files, and how to steer the model toward the outcome I want.

That’s exactly why I’m not willing to pretend the tool alone is enough. Better output does not erase the need for actual judgment.

Foundation still decides who can use AI well

I’m not budging on this part: vibe coding is not okay under any circumstance if you do not have a foundation in software development and architecture. If you do not understand the basics, AI does not magically create them for you.

A few things make up that foundation:

  1. How the web actually works

You need to understand requests, responses, status codes, and what happens between the browser and the server. Without that, you’re just guessing at what the app is doing.

  1. How data is structured

You need to understand how to model data, how relationships work, and what an index is. If the data model is fuzzy, the app will be fuzzy too.

  1. How to read code, not just write it

You need enough hand-written code under your belt that you can read code and actually see what it’s doing, instead of skimming past it like noise.

  1. How debugging feels in real life

You need to have been burned by enough bugs to recognize bad patterns. That kind of pattern recognition does not come from prompting.

You do not need a CS degree. You do not need to memorize sorting algorithms. You do not need to be a genius. But you do need to have built things from scratch, broken them, fixed them, and learned what failure looks like.

That confidence is the dangerous part.

Beginners should use AI to learn faster, not skip learning

I am not saying beginners should stop using AI tools. That ship has sailed, and fighting it is the wrong move. The better move is to change what you use them for.

Use AI to accelerate learning, not to bypass it.

There’s a huge difference between those two things, and that difference is where most people go wrong. If you’re new, let the model explain the code it generates. Type it out yourself. Break it on purpose. Then figure out why it broke.

That process is where the actual growth happens, and it’s also why the developers who will do well over the next few years are not just the ones who can prompt well. They’re the ones who can prompt well, read code, design systems, and debug under pressure.

The AI handles the typing. You still have to handle the thinking.

Vibe coding works best after you’ve earned the right to trust it

Once you’re past the learning stage, vibe away. Seriously. If you already have the foundation, AI can be an incredible force multiplier, and I’ve been using it that way in my own workflow and in my home lab with eight machines managed by OpenClaw.

But it should not become your entire workflow for every project. If you rely on it for everything, you will forget too much and become too dependent on it. That’s where the trouble starts.

My position now is simpler than my old one. I am not against vibe coding anymore. I just think it should be a tool, not the whole system.

The AI handles the typing. You still have to handle the thinking.

And that is the whole line between useful and dangerous: let Claude, GPT-5.5, and the rest speed you up, but never let them replace the part of the job that actually makes you a developer.

This article was adapted from I’ve Changed My Opinion On Vibe Coding

I built an agent that builds agents — runs on local Qwen3, pure Python.

 Hi, I built this agentic AI system.

Closed-loop — takes a raw idea, ships a standalone .py agent file.

What’s different:

  • Interviews you until it understands the request before building anything
  • Two testing stages: prompt validation via LLM invoke, then real subprocess
    execution of generated code. Not the same thing.
  • Self-referential: injects its own source as a reference template for generation
  • Structured rating schema drives iteration. Human approval gate before anything saves.

Runs on Qwen3.6-35B A3B Q8_0 locally via llama.cpp.

GITHUB PAGE

Give it a shot and tell me what you think.

The weekend I fell down the MCP rabbit hole

👋This is the start of a series where I document what I’m learning about Model Context Protocol Architecture and Tool implementations

I’d been reading about MCP for a while and felt like I had a good grasp of the concepts, but I hadn’t actually built anything yet. So this series is me finally putting my hands on it, documenting what I’ve been learning over the last few weeks, and sharing it in case it’s useful to others!

My learning is coming from a mix of things: Some books, a few YouTube tutorials, conversations with DevRel peers exploring similar things, and a fair amount of AI assistance to help me understand concepts along the way 😄

Here’s what I built and what I found out along the way. Let’s start!

An Intro to Model Context Protocol (MCP)

In the MCP Standard book, by Srinivasan Sekarthere is a perfect analogy to explain the problem MCP solves.

Picture this: you have “M” AI models and “N” tools you want to connect to them.

  • The Old Way: Without a standard, you’d have to build a unique integration for every single combination. That’s ‭$M times N$‬‭‬ different connections and a total nightmare to maintain!
  • The MCP Way: MCP acts as the universal plug. You build your tool once to the MCP standard, and it instantly works with any model that speaks it.

Does this scenario sound familiar? It’s what happened with containerization before Kubernetes. Kubernetes came along and said: Here’s the standard. MCP is doing the same thing for AI tooling.

The 3 MCP Roles to Understand

Before building anything, there are three roles to wrap your head around:

  • The Host is the app the user interacts with directly. It decides which tools to call, keeps the conversation context, and manages clients. Think Claude Desktop, Goose, VS Code, Cursor.

  • The Client lives inside the host. It maintains the connection with a concrete MCP Server.

  • The Server answers client requests and exposes tools, resources, and prompts through the MCP protocol.

Before MCP, the AI agent was doing everything (fetching data, formatting it, calling the model, returning the answer. One giant piece of logic. Hard to test and hard to scale). After MCP, the agent only does one thing: reason.

Level 0: Let’s build a Calculator

Not glamorous, but the perfect hello world for understanding the fundamentals

1. Using the MCP Python SDK

To build an MCP server you don’t need to implement the protocol from scratch. Anthropic maintains an official open source SDK (under MIT license) under the modelcontextprotocol organisation on GitHub.

Without the SDK you’d have to manually handle the JSON-RPC messages that flow between host and server via stdin/stdout, implement the handshake, route requests, serialize responses… It’s totally doable, but the SDK abstracts all of that.

There are two ways to build a server inside the Python SDK, and I decided to go with FastMCP option.

💡At this point, you’re setting up the Server side of the architecture. No Host or Client involved yet, just your Python environment and the SDK.

2. Create the Server (server.py)

Not glamorous but the perfect “hello world” for understanding the fundamentals before building something actually useful.

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("calculator")

@mcp.tool()
def multiply(a: float, b: float) -> float:
    """Multiply 2 numbers"""
    return a * b

# + add, subtract, divide...

if __name__ == "__main__":
    mcp.run()

💡Your server.py is the MCP Server. It exposes tools through the protocol. No Host has connected to it yet.

3. Test Server and Tools

Before connecting to any AI host, I tested it with the official MCP Inspector

uv run mcp dev server.py

A browser UI opens. You’ll see: status “Connected”, server name “calculator”, SDK version, and the history.

💡 The MCP Inspector is acting as both Host and Client here: doing the handshake, maintaining the connection, calling your tools

4. Connect to a Host

I tested three options:

🪿 Goose

Goose is an open-source AI agent that can act as an MCP Client and has a visual form to configure the extension name, type (STDIO), and the command it runs every time it starts. Internally, it stores config in a YAML file you never touch.

✳️ Claude Desktop

Claude Desktop needs you to configure a manual JSON such as:

{
  "mcpServers": {
    "calculator": {
      "command": "/path/to/uv",
      "args": ["run", "--directory", "/path/to/project", "python3", "server.py"]
    }
  }
}

🤖 VS Code + Copilot

Same JSON approach as Claude Desktop inside settings.json.

Goose Claude Desktop VS Code + Copilot
Model Llama 3.3-70b Claude Opus 4.6 GPT-4o
Provider Groq Anthropic GitHub Copilot
MCP Config Visual UI → YAML Manual JSON Manual JSON
API Cost Free (14,400 req/day) Pay per use Free tier (50 chats/month)
Requires API Key Yes (Groq) Yes (Anthropic) No (GitHub account)
Local model option Yes (Ollama) No No

💡 Each host manages its own MCP Client internally. You don’t write client code (the host handles that). Your job as a “server developer” is just to make sure your server speaks valid MCP.

5. Full MCP Cycle Overview

As a takeaway, let’s summarize the process into this 8-step cycle:

  • 1 User type the query and the Host receives it
  • 2 Host passes it to the LLM for initial reasoning. LLM decides: “I need an external tool for this”
  • 3 LLM signals tool invocation intent to the Host
  • 4 Host delegates to the Client (the messenger)
  • 5 Client serializes the request in JSON-RPC and sends it to your MCP Server via stdin/stdout
  • 6 Server validates and executes your Python function
  • 7 Server returns a standard MCP response
  • 8 Host shows the result to the user

What’s Next?

The calculator was self-contained (all logic lived in server.py). In my next post, I’ll share my experience in building a GitHub Stats server to talk to an external API and how to handle authentication, rate limits, and what data to expose to the LLM.

Resources

Virtual Machines, Virtual Environments and Containers: Understanding the Differences

INTRODUCTION

When working in software development or data engineering, you will often hear terms like virtual machines, virtual environments and containers. They may sound similar, but they solve different problems.

All three are used to create some form of isolation, but they operate at different levels. Understanding how they differ helps you choose the right tool depending on what you are trying to achieve.

In this article, we will look at what each one is, how they work and the key differences between them.

1. WHAT IS A VIRTUAL MACHINE?

A virtual machine (VM) is a full computer system that runs inside another computer.
It includes:

  • Its own operating system (Linux, Windows, etc.)
  • Virtual hardware (CPU, memory, storage)
  • Applications running inside it

A virtual machine runs using software(a hypervisor) like VirtualBox or VMware, which allows one computer to act like another separate computer inside your main system.
For example, you can run a Linux system inside a Windows laptop using a virtual machine. Even though it is inside your computer, it behaves like a completely separate machine.

Key idea:
A virtual machine is a complete system with its own operating system(OS).

2. WHAT IS A VIRTUAL ENVIRONMENT?

A virtual environment is mainly used in programming, especially in Python. It is used to manage and isolate dependencies for a specific project.

It does not include:

  • An operating system
  • Virtual hardware

Instead, it only isolates:

  • Libraries
  • Packages

For example, one project may require an older version of a library, while another project needs a newer version. A virtual environment allows both to exist without conflicts.

Key idea:
A virtual environment is a lightweight setup for managing project dependencies.

3. WHAT ARE CONTAINERS?

Containers are a way of packaging an application together with everything it needs to run.

They include:

  • Application code
  • Libraries and dependencies
  • Runtime environment

Containers do not include a full operating system. Instead, they share the host system’s kernel, which makes them lightweight and fast.

The most common tool used for containers is Docker.
For example, you can package a web application into a container and run it on any machine without worrying about differences in setup.

Key idea:
A container is a portable package that runs the same everywhere.

4. KEY DIFFERENCES

The main difference between these three comes down to how much they isolate and what they include.

  • A virtual machine includes a full operating system and behaves like a separate computer
  • A virtual environment only manages project-level dependencies
  • A container packages an application and its environment without including a full OS

Because of this:

  • VMs are heavier and use more resources
  • Virtual environments are very lightweight
  • Containers are lightweight but more complete than virtual environments

5. WHEN TO USE EACH

Virtual Machines
When you need to run a different operating system
When strong isolation is required
Example: testing software on different OS platforms

Virtual Environments
When working on programming projects with different dependencies
Common in Python development
Example: managing different versions of libraries

Containers
When deploying applications
When you want consistency across environments
Example: running the same application on different servers

6. PRACTICAL EXAMPLE

Consider a data engineering project:
A virtual machine can be used to run a Linux server on a Windows machine
A virtual environment can be used to manage Python libraries like pandas or numpy
A container can be used to package the entire data pipeline and deploy it easily
Each tool plays a different role, even within the same project.

7. ADVANTAGES AND LIMITATIONS

Virtual Machines
Advantages

  • Strong isolation
  • Can run different operating systems
  • Suitable for testing environments

Limitations

  • Heavy and slower to start
  • Uses more system resources
  • Virtual Environments

Virtual environments
Advantages

  • Lightweight and easy to use
  • Prevents dependency conflicts
  • Ideal for development

Limitations

  • Limited to programming dependencies
  • Does not isolate the full system
  • Containers

Containers
Advantages

  • Lightweight and fast
  • Portable across different systems
  • Consistent environment

Limitations

  • Less isolation compared to VMs
  • Requires understanding of tools like Docker

CONCLUSION

Virtual machines, virtual environments, and containers are all used to create isolation, but they operate at different levels.

A virtual machine provides a full system with its own operating system. A virtual environment focuses only on managing project dependencies. A container packages an application and its environment to ensure it runs consistently across different systems.

Understanding these differences helps in choosing the right tool for the right task. In most modern workflows, all three can be used together to build efficient and reliable systems.

Frameworks Solve UI. Engines Solve Businesses.

Most web frameworks solve one part of the problem really well:
building interfaces.

And to be clear, that matters.

Good frameworks improved developer experience dramatically over the last decade. They gave us component systems, routing, hydration, SSR, state management patterns, and ecosystems that helped developers move faster than ever before.

But after spending years building applications, infrastructure, APIs, deployment systems, and operational tooling, I started noticing a pattern:

Most businesses don’t actually struggle with rendering buttons on a screen.

They struggle with everything around the application.

  • Infrastructure
  • Scaling
  • Deployment
  • Authentication
  • Integrations
  • Permissions
  • Monitoring
  • Operational workflows
  • Data portability
  • Multi-service communication
  • Lifecycle management
  • Vendor lock-in
  • Organizational growth

Frameworks help you build applications.

Businesses need systems that survive growth.

That distinction changed how I think about software entirely.

The Shift From “Pages” to “Systems”

For a long time, I thought in terms of websites and apps.

Now I think in terms of operational ecosystems.

That mental shift is what eventually led me toward building what I now call an engine-based architecture approach with projects like WebEngine, KiwiPress, and the broader CitrusWorx ecosystem.

Not because “frameworks are bad.”

Far from it.

But because eventually I realized something important:

A framework helps you build a product.

An engine helps you operate a business.

Those are not the same thing.

A Framework Usually Ends at the App Layer

Most frameworks are focused primarily on:

  • rendering
  • routing
  • state
  • API consumption
  • build tooling
  • frontend/backend execution

That’s useful.

But businesses eventually need much more than application code.

They need:

  • deployment strategies
  • environment orchestration
  • scalable infrastructure
  • observability
  • role systems
  • workflow automation
  • content pipelines
  • integration pipelines
  • modular expansion
  • operational tooling
  • tenant management
  • data governance
  • blueprinting and repeatability

At some point, the “app” becomes the smallest part of the problem.

The operational system becomes the real product.

The Racecar Problem

One analogy I keep coming back to is this:

A racecar driver shouldn’t need to manufacture every part of the car before racing.

They should be able to:

  • choose proven components
  • assemble systems
  • tune performance
  • focus on execution

Software often does the opposite.

Every team rebuilds:

  • auth
  • CMS layers
  • deployment flows
  • admin systems
  • billing
  • workflow tooling
  • API structures
  • permissions
  • environments
  • integrations

Over and over again.

Not because they want to.

Because most tooling stops at the framework layer instead of the operational layer.

This Is Where “Blueprint Thinking” Started For Me

I started becoming less interested in:

“How do I build another app?”

And more interested in:

“How do I create repeatable operational systems?”

That’s where the idea of blueprints came from.

Not templates.

Not themes.

Operational blueprints.

For example:

  • a pizzeria system
  • a creator storefront
  • a print-on-demand business
  • a course platform
  • a church management system
  • a local service business stack

Each one has:

  • workflows
  • permissions
  • operational states
  • integrations
  • infrastructure needs
  • communication patterns
  • scaling concerns

Businesses aren’t just interfaces.

They’re living operational systems.

Engines Think Beyond the Frontend

This is where I think engines become interesting.

An engine doesn’t just ask:

“How do we render this page?”

It asks:

  • How does this deploy?
  • How does this scale?
  • How does this integrate?
  • How does this evolve?
  • How does this stay maintainable?
  • How does this remain portable?
  • How do teams operate this safely?
  • How do we reduce hidden complexity?
  • How do we create repeatable systems?

That’s a much larger problem space.

But I think it’s the problem space the industry is gradually moving toward.

Especially as infrastructure, AI, automation, and distributed systems become more intertwined with everyday application development.

Why This Matters More in the AI Era

AI is making it easier than ever to generate application code quickly.

But speed alone doesn’t solve operational architecture.

If anything, I think it exposes weak architecture faster.

Because generated code still has to:

  • scale
  • integrate
  • remain secure
  • remain understandable
  • remain maintainable
  • survive organizational growth

That’s why I’ve become increasingly interested in:

  • contracts
  • explicit systems
  • pipelines
  • modular runtimes
  • infrastructure-aware development
  • declarative architecture

Not because they’re trendy.

Because operational clarity matters.

Especially when systems become large.

I Don’t Think the Future Is “No-Code”

And I don’t think it’s “AI builds everything.”

I think the future is better operational abstraction.

Systems where:

  • infrastructure is more intentional
  • architectures are more explicit
  • workflows are more composable
  • businesses can move faster without sacrificing clarity

That’s ultimately the direction I’m exploring with CitrusWorx and WebEngine.

Not replacing frameworks.

Building around the parts frameworks usually leave behind.

Final Thoughts

Frameworks changed the web.

No question.

But I think the next evolution isn’t just about building apps faster.

It’s about building operational systems more intelligently.

Because eventually every successful application becomes:

  • infrastructure
  • workflows
  • operations
  • governance
  • scaling
  • lifecycle management

In other words:

Eventually every app becomes a business system.

And business systems need more than UI rendering.

They need engines.

Gemma 4: models and setup

**

Gemma 4 Model

**

Google’s Gemma family has quickly become one of the most practical and developer-focused open-weight AI ecosystems available today. With the release of Gemma 4, Google has introduced major improvements over Gemma 3, making it the company’s most advanced open model family so far.

But Gemma 4 is more than just another language model update. It reflects a broader move toward accessible AI that developers, researchers, students, and independent creators can run, customize, and experiment with directly on their own machines.

That changes everything.

What Is Gemma 4?

Gemma 4 is the newest lightweight open-weight AI model family developed by Google DeepMind.

The main goal behind the release is to improve reasoning abilities while maintaining efficient performance, faster response generation, and better support for complex multi-step tasks.

The Gemma 4 Model Lineup

The Gemma 4 family currently includes:

  1. Gemma 4 E2B
  2. Gemma 4 E4B
  3. Gemma 4 26B-A4B
  4. Gemma 4 31B

  5. Gemma 4 E2B

Gemma 4 E2B is the smallest model in the lineup. It is built for:

  • low memory usage
  • fast inference speeds
  • and edge deployment environments.

The model works well on:

  • laptops,
  • Raspberry Pi devices,
  • embedded systems,
  • and lightweight offline applications.

Why It’s Important

Smaller AI models traditionally struggled with reasoning quality and consistency. Gemma 4 E2B demonstrates how much compact architectures have improved.

Even with minimal hardware, the model can:

  • summarize notes
  • answer questions,
  • assist with coding tasks,
  • and operate entirely offline.

Recommended Hardware

  • 4–6GB RAM
  • Low-VRAM GPUs
  • Apple Silicon devices
  • Small edge AI hardware

Best Use Cases

  • Offline AI assistants
  • Educational applications
  • AI-powered note summarizers
  • Smart home automation
  • Lightweight chatbot systems
  1. Gemma 4 E4B

Why E4B Stands Out

The E4B model is widely considered the sweet spot between:

  • speed,
  • reasoning quality,
  • overall performance,
  • and hardware efficiency.

For many developers, this is likely the model they’ll use most often.

Key Strengths
E4B performs especially well in:

  • coding tasks,
  • reasoning,
  • long-form conversations,
  • summarization,
  • and RAG-based systems.

Recommended Hardware

  • RTX 3060 / 4060 or better
  • Apple Silicon Macs
  • 8–12GB VRAM
  • 16GB+ system RAM
  • Best Use Cases
  • AI coding assistants
  • Research applications
  • Personal AI tools
  • Local productivity systems
  • Chat-based applications
  1. Gemma 4 26B-A4B

What Makes It Different?

This model uses a Mixture-of-Experts (MoE) architecture.

Instead of activating the entire neural network for every token, it selectively activates specialized expert layers only when needed.

Why MoE Matters

MoE architectures improve:

  • efficiency,
  • scalability,
  • and inference performance.

Main Advantages

  • Faster inference
  • Reduced compute costs
  • Strong reasoning performance
  • Better scaling efficiency

Recommended Hardware

  • RTX 4090
  • Multi-GPU systems
  • 24–48GB VRAM
  • High-performance workstations

Best Use Cases

  • AI agents
  • Research environments
  • Advanced coding systems
  • Long-context workflows
  • Autonomous AI pipelines
  1. Gemma 4 31B

*The Flagship Model
*

Gemma 4 31B is the most powerful dense model in the family.

It is designed for:

  • advanced reasoning,
  • complex instruction handling,
  • multimodal workflows,
  • and enterprise-scale AI applications.

Why Dense Models Still Matter

Dense models are often preferred because they provide:

  • more stable outputs,
  • strong reasoning capabilities,
  • and more consistent responses.

The 31B model focuses heavily on maximizing output quality rather than only optimizing efficiency.

Features

  • 256K context window
  • Multimodal support
  • Advanced reasoning
  • Long-form text generation
  • Strong coding performance

Recommended Hardware

  • RTX 4090 / A100 / H100
  • 32GB+ VRAM
  • Quantized inference support
  • High-end workstation setups

Multimodal Capabilities

Gemma 4 models also support multimodal workflows.

That means they can process:

  • text,
  • images,
  • and audio.
    Why Multimodal AI Is Important

This opens the door for applications such as:

  • visual tutoring systems,
  • image analysis,
  • accessibility tools,
  • UI understanding,
  • and document interpretation.

Running Gemma 4 Locally

One of the biggest reasons Gemma 4 is gaining popularity is how easy it is to run locally. Unlike many large AI systems that require expensive cloud infrastructure, Gemma 4 can operate directly on personal hardware using tools like Ollama.

This allows developers to:

  • experiment more freely,
  • avoid API costs,
  • work offline,
  • and improve privacy because data stays on the local machine.

Local AI development is becoming increasingly important for:

  • students learning AI,
  • independent developers,
  • researchers,
  • and startups building prototypes.

Installing Gemma 4 with Ollama

Ollama offers one of the easiest ways to download and run local AI models.

After installing Ollama, you can pull Gemma 4 directly from the terminal.

Install Gemma 4

ollama pull gemma:4b

This command downloads the model weights and prepares the model for local inference.

Depending on your hardware and internet connection, the process may take several minutes.

Running the Model

Once installation is complete, you can start using the model immediately.

ollama run gemma:4b

Ollama will launch an interactive terminal session where you can type prompts directly.

Example:

>>> Explain neural networks in simple words

The model then generates responses locally on your device.

_Using Gemma 4 in Python Applications
_

Gemma 4 can also be integrated into Python applications very easily.

This is useful for:

  • chat applications,
  • AI assistants,
  • research tools,
  • automation software,
  • and web applications.

Python Example

from ollama import chat

response = chat(
   model='gemma:4b',
   messages=[
       {
           'role': 'user',
           'content': 'Explain transformers simply'
       }
   ]
)

print(response['message']['content'])

Understanding the Code

Importing the Chat Function

from ollama import chat

This imports Ollama’s chat interface into Python and allows your application to communicate with the local Gemma model.

Sending a Prompt

response = chat(
   model='gemma:4b',
   messages=[
       {
           'role': 'user',
           'content': 'Explain transformers simply'
       }
   ]
)

Here:

model='gemma:4b' selects the model,

role='user' identifies the speaker,

and content contains the prompt being sent.

The structure is very similar to modern chat-based AI APIs.

Printing the Response

print(response['message']['content'])

This extracts the generated text from the response and prints it to the console.

Why Local AI Development Matters

Running Gemma 4 locally changes the development experience in several important ways.

Privacy
Your prompts and data remain on your own machine.

Lower Costs
There are no token-based API fees.

Faster Experimentation
Developers can test ideas immediately without worrying about cloud usage limits.

Offline Access
Once installed, the model can operate without an internet connection.

Final Thoughts

One of Gemma 4’s biggest strengths is its accessibility. Only a few years ago, running advanced AI models required enterprise-grade infrastructure, complex CUDA configurations, and expensive GPUs.

Today, developers can:

  • download a model,
  • run it locally,
  • and build AI-powered applications within minutes.

That level of accessibility is one of the main reasons local AI development is growing so rapidly.

Stripe vs Paddle vs Lemon Squeezy: What Wiring All Three Behind One Protocol

Stripe Paddle Lemon Squeezy
Direct processor. Fastest to set up. You handle your own VAT and sales tax. Fits US-focused SaaS, or any product that already has tax infrastructure in place. Merchant of Record for higher volumes. Provides invoicing, revenue reports, and enterprise billing flows. Fits B2B SaaS or companies that need formal financial tooling and VAT handled at scale. Merchant of Record. Handles global tax on your behalf. Fits indie developers selling digital products internationally, where compliance matters more than optimizing fees.

I wired all three into the same codebase. Here is what you find out only by doing it.

Different billing providers have header names and verification methods. That’s about the thing you can count on, but what about the rest? Let’s take a look:

  • Stripe’s webhook signature comes in the Stripe-Signature header.
  • Paddle’s webhook signature comes in the Paddle-Signature header. It looks like this: ts=1704067200;h1=abc123def456.
  • Lemon Squeezy’s webhook signature comes in the X-Signature header.

When you connect one billing provider you just learn its API, but when you connect three providers behind the same interface you really learn what billing is all about. You see what it looks like under all the software development kits (SDKs) and provider documentation.

The problem with integrating each provider

Most billing code is written using a providers SDK. It works fine until your business needs to switch providers or you want to run your tests without hitting a live API.

That’s when you realize that your checkout logic, webhook handlers and subscription queries are all mixed up with Stripe’s details. If you want to switch providers you have to rewrite the application layer.

A better approach would be to figure out what billing really means before deciding which provider to use.

The six-method protocol

In Python, a Protocol defines a contract through structural subtyping. Any class implementing the right methods satisfies it, without inheritance.

For billing, six methods cover the full lifecycle:

class BillingProvider(Protocol):
    async def list_plans(self) -> list[PlanResponse]: ...

    async def create_checkout(
        self, organization_id: str, plan_id: str,
        success_url: str, cancel_url: str

    ) -> CheckoutResponse: ...
    async def create_portal_session(
        self, organization_id: str, return_url: str
    ) -> PortalResponse: ...

    async def get_subscription(
        self, organization_id: str
    ) -> SubscriptionResponse | None: ...

    async def cancel_subscription(
        self, organization_id: str
    ) -> SubscriptionResponse: ...

    async def handle_webhook(
        self, payload: bytes, signature: str
    ) -> dict: ...

The domain layer relies on the protocol. We have three adapters that use this protocol. Our application does not directly use Stripe, Paddle or Lemon Squeezy. It only uses a BillingProvider.

What implementing three adapters reveals

Webhook verification

Stripe delegates to its SDK:

event = stripe.Webhook.construct_event(
    payload, signature, self.config.webhook_secret
)

Paddle uses a custom header format: ts=1704067200;h1=abc123. You split it, extract timestamp and hash, then verify manually:

parts = dict(part.split("=", 1) for part in signature.split(";"))
ts = parts.get("ts", "")
h1 = parts.get("h1", "")

expected = hmac.new(
    secret.encode(),
    f"{ts}:{payload.decode()}".encode(),
    hashlib.sha256
).hexdigest()

if not hmac.compare_digest(expected, h1):
    raise ValueError("Invalid Paddle webhook signature")

Lemon Squeezy uses a direct HMAC-SHA256 over the raw payload:

expected = hmac.new(
    secret.encode(),
    payload,
    hashlib.sha256
).hexdigest()

if not hmac.compare_digest(expected, signature):
    raise ValueError("Invalid Lemon Squeezy webhook signature")

Here is the rule for all three: you have to check the bytes before you do any JSON parsing. The signature is for the payload. If you parse the JSON first and then check it does not work because the JSON can change the bytes without you noticing.

Event types

After you have checked the payload you still need to know what is going on. Each provider names event types differently.

What happened Stripe Paddle Lemon Squeezy
Payment succeeded checkout.session.completed transaction.completed order_created
Subscription created customer.subscription.created subscription.created subscription_created
Subscription cancelled customer.subscription.deleted subscription.canceled subscription_cancelled
Payment failed invoice.payment_failed transaction.payment_failed subscription_payment_failed

Notice that Lemon Squeezy uses subscription_cancelled with two l's (the L letter), while Paddle uses subscription.canceled with only one l. A silly change that can make you debug code for hours.

Checkout URL location

You’ve created the checkout. Now you need the URL to redirect the user. Each provider puts it somewhere different.

Stripe: session.url

Paddle nests it inside a checkout object, but not reliably:

url = result.get("checkout", {}).get("url") or result.get("url", "")

Lemon Squeezy uses JSON:API format, so everything lives under data.attributes:

url = data["data"]["attributes"]["url"]

Portal sessions

The portal page is where customers can upgrade or downgrade or cancel. This page works differently depending on who is providing it.

Stripe requires creating a session before returning a URL:

session = await stripe_client.billing_portal.sessions.create_async(
    customer=customer_id,
    return_url=return_url
)
return PortalResponse(url=session.url)

Paddle returns the portal URL directly on the customer object. No session creation:

customer = await self._get_customer(organization_id)
return PortalResponse(url=customer.portal_url)

Lemon Squeezy stores it nested under customer.urls:

portal_url = customer_data["attributes"]["urls"]["customer_portal"]

Status values

Subscription statuses look the same until they don’t:

Stripe Paddle Lemon Squeezy
active active active
trialing trialing on_trial
past_due past_due past_due
canceled canceled cancelled
— paused paused
incomplete — —

trialing versus on_trial. canceled versus cancelled.

These won’t cause an error. They can cause your query for trials to return zero rows on two out of three providers, and you will spend an afternoon figuring out why.

Normalize at the adapter boundary – what enters the database should come from your vocabulary, and should not be copied from provider documents.

Plan feature metadata

Each provider has a different convention for storing the feature list shown on a pricing page.

Stripe uses marketing_features on the product, falling back to product metadata:

features = [f.name for f in product.marketing_features] 
    if product.marketing_features 
    else product.metadata.get("features", "").split(",")

Paddle uses custom_data, falling back to description lines:

features = product.custom_data.get("features", []) 
    if product.custom_data 
    else [line for line in product.description.split("n") if line.strip()]

Lemon Squeezy parses variant description lines directly.

None of these options are wrong. They show three ways to think about where information about features is stored. The adapter keeps this hidden from the rest of the application.

What the abstraction makes visible

When we look at three adapters one thing becomes clear: the differences are not about the business logic.

The core ideas common for all three providers are the same:

  • how to define a subscription
  • when it starts
  • when it ends
  • is it active or cancelled

The differences are about how we connect to them: the names of headers, the structure of URLs, the words we use for events, the strings we use for status.

Why hexagonal architecture

The hexagonal architecture draws a line between these two concerns. The main part of the application does not know what a Stripe-Signature is. The main part of the application knows what a SubscriptionResponse is.

This has an effect on testing. With the protocol as the boundary each adapter can be tested alone using made payloads and real HMAC signatures. We do not need API accounts and we do not need to mock providers that might not behave like they do in production. All 110 tests across the domain, providers, configs, router, webhook and repository layers can run offline.

Choosing between the three

Once the architecture is in place choosing a provider is a business decision. The technical differences are real and quite limited.

Stripe has the mature software development kit and the best documentation. It also has the complex rules to follow when selling outside the US.

Paddle and Lemon Squeezy are both services that handle payments and taxes for us, send VAT and sales tax on our behalf. If we are selling to customers in the EU and do not want to register for tax in countries this matters even more.

Lemon Squeezy has the simplest way of working. JSON:API is a bit wordy but consistent. The webhook verification is the simplest of the three to do correctly.

Paddle has the flexible pricing models. We can charge based on usage, seats or custom plans. The ts=...;h1=... Signature format is the trickiest to get right and the easiest to get wrong without noticing.

If you take one thing from each section

First write down the six protocol methods as ideas before you look at the Stripe documentation. This one step helps you understand what billing means in your code without getting confused with what Stripe calls things. If you think about Stripe early it can mess up your understanding of what billing means.

Make status strings and event types consistent at the adapter boundary. The webhook handler should not see things like on_trial or subscription_cancelled.

When you check signatures use the bytes, not a re-serialized dictionary. If you convert JSON back and forth it can quietly change the bytes. That can cause a really bad kind of bug if someone tampers with the payload.

Do not pretend to check webhook signature verification in tests. Instead create a HMAC and use the real verification function. The Paddle format has cases that a pretend version will not catch, like what happens when the ts key is there but empty.

These patterns took me a while to get right. If you’d rather skip that, Billing Foundation is a paid starter kit that ships all three adapters already wired behind this Protocol, with 110 tests, a FastAPI backend, Next.js 16, PostgreSQL, Redis, Docker Compose, and an interactive (12-section) step-by-step tutorial runs in any browser.

You can start with a billing layer that you know works instead of debugging special cases like ts=...;h1=....

What’s your experience ?

If you’re running billing in production: which provider, and what’s the edge case you hit that the documentation didn’t mention?