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!
