← Back to list

Compile Once, Evaluate Many: Using OPA Correctly in Production

When working with Open Policy Agent (OPA), it’s easy to assume that performance issues stem from complex policy logic. That was my…

Sushan Sapaliga · 2026-02-05 17:24 · 1 claps · 2.9 min read
#open-policy-agent #optimization #golang #rego #rule-engine
Open on Medium ↗
Wiki topics: AGT · AI Agents

Compile Once, Evaluate Many: Using OPA Correctly in Production

When working with Open Policy Agent (OPA), it’s easy to assume that performance issues stem from complex policy logic. That was my assumption too — until I realized the real problem had nothing to do with the policies themselves.

It was how the Rego engine was being initialized.

This post focuses on a production pattern that is rarely discussed but critically important:

OPA policies must be compiled once and reused — not initialized per request.

If you already understand what OPA is and how Rego works, this post is for you.

For readers new to OPA or looking for general use cases, this article by Chathura Gunasekera provides a good introduction — Implementing Policies with OPA — Example Use Cases.

This post intentionally skips the basics and dives straight into runtime behavior and performance.

Why Initialization Strategy Matters

OPA is often used in request paths that are:

  • latency-sensitive
  • high-throughput
  • evaluated per API call

Because of that, how policies are loaded and prepared matters just as much as what the policies do.

At a high level, OPA does two very different kinds of work:

  1. Policy compilation (expensive)
  2. Policy evaluation (cheap)

Problems start when these two get mixed up.

The Mistake: Initializing Rego Per Request

A very common (and tempting) approach looks like this:

func authorize(input Input) bool {
    r := rego.New(
        rego.Query("data.authz.allow"),
        rego.Load([]string{"./policies"}, nil),
    )

    rs, err := r.Eval(context.Background(), rego.EvalInput(input))
    if err != nil {
        return false
    }

    return rs.Allowed()
}

This code works. But it hides a serious problem.

Every time this function is called:

  • Rego files are loaded
  • Policies are parsed
  • ASTs are built
  • Compilation happens again

If this runs per request, you are recompiling your entire policy set on every call.

What Actually Happens Inside OPA

To understand why this hurts performance, it helps to know what OPA does internally.

When policies are loaded, OPA:

  1. Parses Rego source files
  2. Builds an abstract syntax tree (AST)
  3. Compiles rules into an internal representation
  4. Prepares the query execution plan

This process is CPU-intensive and memory-heavy — and it is not meant to run on every request.

OPA is designed with the assumption that:

Policies change rarely, inputs change frequently.

The Correct Model: Compile Once, Evaluate Many

The correct approach is to separate:

  • policy lifecycle
  • request lifecycle

Conceptually

Application startup:
    load policies
    compile rego
    prepare query

Per request:
    evaluate prepared query with input

A Practical Initialization Pattern (Go Example)

Below is a simplified example showing the correct approach.

Step 1: Initialize OPA at startup

var preparedQuery rego.PreparedEvalQuery

func initOPA() error {
    r := rego.New(
        rego.Query("data.authz.allow"),
        rego.Load([]string{"./policies"}, nil),
    )

    pq, err := r.PrepareForEval(context.Background())
    if err != nil {
        return err
    }

    preparedQuery = pq
    return nil
}

This function should be called:

  • at application startup
  • or when policies are updated

Step 2: Evaluate per request

func authorize(input Input) (bool, error) {
    rs, err := preparedQuery.Eval(
        context.Background(),
        rego.EvalInput(input),
    )
    if err != nil {
        return false, err
    }

    if len(rs) == 0 {
        return false, nil
    }

    allowed, ok := rs[0].Expressions[0].Value.(bool)
    return ok, nil
}

Here:

  • No policies are reloaded
  • No compilation happens
  • Only input data changes

This is the intended production usage model for OPA.

Example: Simple Go Server — demonstrates a simple Go HTTP server integrated with Open Policy Agent (OPA) for request authorization.

A Related Pitfall: Large Bundles and Test Policies

While investigating initialization behavior, another related issue became apparent.

OPA loads everything it is given:

  • all Rego files
  • all data files
  • all test policies

Even if test rules are never queried in production, they:

  • increase compilation time
  • increase memory usage
  • amplify the cost of per-request initialization

This means:

  • Shipping large test policies to production
  • Using oversized bundles
  • Recompiling per request

…all compound into severe performance problems.

Keeping production policy bundles minimal is just as important as reusing compiled queries.

A Practical Go Example (with Runnable Code)

To make this concrete, I created a small GitHub repository with runnable examples: **https://github.com/sushansapaliga/opa-go-examples**

The repository contains:

  • a latency benchmark
  • a simple Go HTTP server
  • examples of correct and incorrect OPA initialization patterns

Closing Thoughts

OPA is fast, reliable, and production-ready — but only if used the way it was designed.

Most performance issues are not caused by “slow policies”. They are caused by doing expensive work repeatedly when it should only happen once.

If you’re debugging OPA latency issues, don’t just look at your Rego logic. Look at when and how the engine is initialized.

That’s often where the real problem lies.


메타데이터
post_id
039bea4ddf25
slug
compile-once-evaluate-many-using-opa-correctly-in-production-039bea4ddf25
url
https://medium.com/@sushansapaliga/compile-once-evaluate-many-using-opa-correctly-in-production-039bea4ddf25
canonical_url
https://medium.com/@sushansapaliga/compile-once-evaluate-many-using-opa-correctly-in-production-039bea4ddf25
author_url
https://medium.com/@sushansapaliga
status
ok
fetched_at
2026-07-13 06:23:13