Ever waited for PR preview environments to spin up? Yeah, me too. Here's a pattern that changed the game for our team: pre-configured deployment slots with deterministic routing.
The Problem
Traditional PR preview workflows go something like this:
- Open PR
- CI/CD provisions a new environment
- Wait... ⏳
- Deploy code
- Wait some more... ⏳
- Finally get your preview URL
The provisioning step is the killer. Whether you're using Kubernetes namespaces, cloud functions, or edge workers, creating resources takes time.
The Solution: Pre-Configured Slots
What if we flipped the script? Instead of creating environments on-demand, we pre-configure a fixed set of deployment slots:
tokyo 🔗 https://tokyo.example.com
paris 🔗 https://paris.example.com
london 🔗 https://london.example.com
berlin 🔗 https://berlin.example.com
sydney 🔗 https://sydney.example.com
madrid 🔗 https://madrid.example.com
moscow 🔗 https://moscow.example.com
cairo 🔗 https://cairo.example.com
dubai 🔗 https://dubai.example.com
rome 🔗 https://rome.example.com
Then use a deterministic hash to map PR numbers to slots:
- uses: kriasoft/pr-codename@v1
id: pr
- run: wrangler deploy --env ${{ steps.pr.outputs.codename }}
PR #1234 always maps to tokyo
. PR #1235 always maps to paris
. No provisioning, no waiting.
How It Works
The magic happens in three parts:
1. Pre-Configure Your Slots
First, set up your deployment slots. Here's a Cloudflare Workers example:
# wrangler.toml
[env.tokyo]
name = "preview-tokyo"
route = "tokyo.example.com/*"
[env.paris]
name = "preview-paris"
route = "paris.example.com/*"
[env.london]
name = "preview-london"
route = "london.example.com/*"
# ... repeat for all slots
2. Deterministic Mapping
The PR Codename Action uses a simple hash function to consistently map PR numbers to slot names:
const words = ["tokyo", "paris", "london", "berlin" /* ... */];
const index = prNumber % words.length;
return words[index];
The above is just an example, in reality it uses FNV-1a hashing algorithm.
3. Deploy to the Slot
Your GitHub Action workflow becomes dead simple:
name: Deploy PR Preview
on:
pull_request:
types: [opened, synchronize]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: kriasoft/pr-codename@v1
id: pr
- name: Deploy to slot
run: |
npm ci
npm run build
wrangler deploy --env ${{ steps.pr.outputs.codename }}
- name: Comment PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '🚀 Preview deployed to https://${{ steps.pr.outputs.codename }}.example.com'
})
The Benefits
This pattern isn't just a neat trick; it fundamentally changes the rhythm of your development cycle.
🚀 Zero-Wait Deploys The biggest win. By eliminating the on-demand provisioning step, deployments start immediately. What used to be a 2-3 minute coffee break is now a 30-second task. Your developers stay in the flow, and your pipeline gets a whole lot faster.
🔗 URLs You Can Actually Share Forget long, ugly, auto-generated URLs. With this pattern, PR #1234 always maps to https://tokyo.example.com
. This URL is:
- Memorable: You can actually remember it.
- Shareable: Perfect for dropping in a Slack channel, a Jira ticket, or even saying out loud during a Zoom call. No more "Hey, can you find that preview link for me?"
- Bookmarkable: QA testers and product managers can bookmark slots for features they're tracking.
💰 No More Cloud Bill Surprises Dynamic environments are notorious for leaving behind orphaned resources that quietly drain your budget. With a fixed number of slots, your infrastructure costs become predictable. You know exactly what's running, and you never have to hunt down forgotten preview apps again.
🧹 Cleanup? What Cleanup? When a PR is merged or closed, there's no complex teardown script to run. The slot simply becomes available for the next PR. You can even have a workflow that automatically deploys the main
branch to the slot to keep it fresh. It's a self-cleaning system.
Real-World Considerations
How Many Slots?
We've found 10-15 slots work well for most teams. The math:
- 10 slots + 50 open PRs = each slot serves ~5 PRs
- Only the latest deployment to each slot is accessible
- Most teams only actively review a handful of PRs at once
Collision Handling
Yes, PRs can map to the same slot. PR #1 and PR #11 both map to the same environment with 10 slots. This means newer deployments overwrite older ones—so if you're reviewing PR #1 and someone pushes PR #11, your preview disappears.
In practice, this works for many teams because:
- Developers typically work on recent PRs
- Old PR previews naturally expire
- You can always trigger a redeploy to refresh
When slots don't work well: Large teams, high PR velocity, or when multiple people need to review the same PR simultaneously.
Database & Stateful Services
The biggest challenge with any preview environment is handling databases and stateful services. With slots, you have a few options:
- Shared database: Fast and cheap, but schema migrations from one PR can break others
- Database per slot: Better isolation, but requires seeding data for each slot
- Database branching services: Tools like Neon offer instant database branches (premium option)
For simple stateless apps, this isn't an issue. For complex apps with databases, it's the main implementation challenge.
Security Notes
- Use environment-specific secrets for each slot
- Consider adding basic auth to preview domains
- Implement automatic cleanup for stale deployments
Beyond Basic Previews
This pattern unlocks some cool possibilities:
Persistent Test Environments: QA can bookmark specific slots for testing.
A/B Testing: Map feature flags to slots for instant switching.
Geographic Testing: Actually deploy slots to different regions.
Try It Yourself
Getting started is pretty straightforward:
-
Install the action:
- uses: kriasoft/pr-codename@v1 id: pr
-
Use the codename in your deploy:
deploy --env ${{ steps.pr.outputs.codename }}
-
Enjoy instant PR previews 🚀
The full source is on GitHub if you want to customize the word list or hashing algorithm.
Slots vs On-Demand: Quick Comparison
Before you dive in, it's worth understanding how the pre-configured slots pattern stacks up against the traditional on-demand ephemeral environments. While this post focuses on slots, knowing the trade-offs helps you make the right choice for your team.
Factor |
Pre-Configured Slots |
On-Demand Ephemeral |
---|---|---|
Setup Speed |
⭐⭐⭐⭐⭐ Instant (pre-warmed) |
⭐⭐⭐ Takes minutes (provisioning) |
Cost Predictability |
⭐⭐⭐⭐⭐ Fixed monthly cost |
⭐⭐ Variable usage-based |
Scalability |
⭐⭐ Hard limit on concurrent PRs |
⭐⭐⭐⭐⭐ Scales with team size |
Isolation |
⭐⭐ PRs can overwrite each other |
⭐⭐⭐⭐⭐ Each PR gets own environment |
Production Fidelity |
⭐⭐⭐ Prone to environment drift |
⭐⭐⭐⭐⭐ Clean slate every time |
Maintenance |
⭐⭐⭐⭐ Simple for basic apps |
⭐⭐ Complex (build) / ⭐⭐⭐⭐ (buy) |
Developer Experience |
⭐⭐ Can be confusing/frustrating |
⭐⭐⭐⭐⭐ Smooth parallel workflows |
Best for Slots: Small teams, simple apps, tight budgets
Best for On-Demand: Growing teams, complex apps, quality-focused
Nothing prevents you from mixing both approaches — use slots for rapid prototyping and on-demand for critical features.
Wrapping Up
Sometimes the best optimization is avoiding work altogether. By pre-configuring deployment slots and using deterministic routing, we eliminated the biggest bottleneck in our PR workflow.
Give it a shot and let me know how it works for your team. Happy deploying!
What patterns have you used for PR previews? Drop a comment below 👇 always curious to hear different approaches!