Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Semaphore Laravel Package

symfony/semaphore

Symfony Semaphore provides a mechanism for managing semaphores, letting concurrent processes coordinate and gain exclusive or weighted access to shared resources. It supports expiration and both System V and Redis-backed storage, making it easy to prevent race conditions in PHP applications.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit

symfony/semaphore is a framework-agnostic Symfony component, not a Laravel package. There is no service provider, facade, config file, or published config — it is a library of ~4 classes (SemaphoreFactory, SemaphoreInterface, Semaphore, plus store adapters) that compiles into Laravel cleanly because Laravel already runs on Symfony primitives. That is both the appeal and the constraint: integration cost is a thin internal wrapper, not a package install.

The architectural question a TPM must force is counting semaphore vs. binary lock, because Laravel already ships Cache::lock() / Redis::funnel and most "do we need this?" requests are actually satisfied by what is already in the box. The defensible use cases are the ones where the answer is N, not 1:

  • Cap concurrent expensive operations per tenant (e.g., max 3 concurrent report renders, max 2 outbound calls to a partner API).
  • Bound queue worker concurrency against a downstream resource rather than against CPU.
  • Guard non-transactional critical sections (external API side effects, counter reconciliation) that DB transactions cannot cover.
  • Throttle by permit count rather than by rate window — RateLimiter answers "how many per minute," not "how many in flight."

If the requirement is plain mutual exclusion, this package is redundant and should be rejected on that basis.

Integration feasibility

Composer-installable, no framework coupling, MIT-licensed, no transitive weight beyond the store layer (symfony/lock and a Redis client). It fits Laravel's container as a singleton with roughly ten lines of binding code. The real friction is version alignment: Laravel 10/11/12 pin symfony/* at ^6.4/^7.x while the tracked target here is v8.1.0-BETA3, which requires PHP >= 8.4. A pre-release on a brand-new major is not a production baseline for most Laravel estates. Feasible — but only on the stable 6.4/7.4 line, which means the 8.1 features (lock-backed store, SemaphoreKeyNormalizer) are out of reach.

Technical risk

  • Adoption signal is unreliable, in both directions. 37 stars and "0 dependents" are artifacts of the read-only split-repo mirror; real consumption flows through symfony/symfony. A TPM must not read this as "no adoption" (false negative) nor as "battle-tested by proxy" without checking Packagist download stats and the monorepo's issue history.
  • Pre-release exposure. Tracking -BETA3 with no documented GA exit criteria is a schedule risk. v8.1.0-BETA3's changelog entry is "various fixes and hardenings," which is a warning that the surface was still moving.
  • Security release on an EOL branch. v5.4.54 is tagged [SECURITY] yet its only changelog line is a PHP 7.2 polyfill declaration — a legacy-hygiene patch, not a behaviour change. Two risks: (a) teams may over-react to the [SECURITY] tag, and (b) teams on 5.4 are on an unsupported branch and should treat this as a migration forcing function, not a routine bump. Confirm the advisory ID and patched-version list; do not assume the 5.4 patch covers the line you run.
  • TTL is a safety net, not a correctness guarantee. An expiring permit can be handed to a second holder while the first is still inside the critical section (long GC pause, slow HTTP call). Semaphore semantics here are "best-effort mutual exclusion with deadlock protection," and the business must accept that trade-off explicitly.
  • Blocking acquire() in the request path consumes a PHP-FPM/Octane worker while waiting. Under load this converts a contention problem into an availability problem.
  • No lease renewal. Long-running jobs must either fit inside the TTL or implement heartbeat/re-acquire logic themselves.

Key questions

  1. Which Symfony line are we pinned to, and does that line satisfy Laravel's existing symfony/* constraints without a composer update cascade?
  2. Is the requirement genuinely N-concurrent, or is it mutual exclusion that Cache::lock() already solves?
  3. Are we willing to ship a -BETA component into production, and who signs off if so? What is the exit criteria to stable 8.1?
  4. What is the TTL for each key, and what is the worst-case duration of the critical section? If max duration > TTL, the design is broken.
  5. Do we have Redis, and is it cluster/sentinel (DSN support landed in 6.4.19/7.2.4)? Single-node only is a scaling cliff.
  6. What happens when Redis is unavailable — fail fast, or degrade? Who owns that decision?
  7. Is this in the SBOM/SCA scan, and who triages Symfony security advisories within SLA?
  8. Do we run Octane? If yes, "automatic release on destruction" is not a release mechanism and must be replaced with explicit finally blocks.

Integration Approach

Stack fit

Target Laravel 10+ on PHP 8.1–8.4, pinned to the stable symfony/semaphore line that matches the framework's existing Symfony constraint (likely ^6.4 or ^7.x). Backend should be Redis reusing the app's existing connection; flock is only acceptable for single-host, non-containerised deployments and should be ruled out for anything running on more than one pod. The store abstraction means the backend is a config decision, not a code decision — keep it swappable.

Migration path

Do not let this land as a direct dependency inside job classes. Wrap it:

  • Define an internal ConcurrencyGuard contract (run(string $key, int $permits, Closure $critical): mixed) owned by the platform team, with a finally-based release and a hard timeout.
  • Implement it once against symfony/semaphore; keep the existing Cache::lock() implementation behind the same interface so the backend is reversible.
  • Migrate call sites one at a time — start with the most expensive, lowest-volume critical section (report generation, partner API fan-out), not the highest-traffic one.
  • Only after the wrapper is proven should ad-hoc SETNX/Cache::lock usages be consolidated onto it. That consolidation is where the real cleanup value is; the component itself is the small part.

Compatibility

Validate the constraint graph before committing: composer why-not symfony/semaphore ^X against the current lock file. Watch for symfony/lock and predis/predis version interplay (Predis 2.x support was itself a bugfix in the 5.4/6.4/7.0 lines). Redis cluster/sentinel DSN handling requires 6.4.19+/7.2.4+ — a silent failure mode on older patches. Do not adopt 8.1 without a PHP 8.4 upgrade already scheduled.

Sequencing

  1. Dependency inventory entry + SBOM/SCA enrolment + advisory subscription (parallel, week 1).
  2. Two-day spike on a non-production queue: verify acquire/release under concurrency, measure Redis round-trip, confirm TTL behaviour under a killed process.
  3. Ship the ConcurrencyGuard wrapper behind a feature flag with the existing implementation as default.
  4. Pilot one low-volume critical section; instrument acquire latency, wait time, timeout rate, and permit utilisation.
  5. Gate expansion on a documented decision record for TTL values and failure behaviour per key.

Operational Impact

Maintenance

Low code churn — the component's public API has been stable across 6.4/7.x/8.x, and releases are largely "no significant changes." The maintenance burden sits upstream: the repo is a read-only split of symfony/symfony, so issues and PRs route to the monorepo and the package's own tracker is not a support channel. Upgrades ride the Symfony minor cadence; budget for a recurring review of advisories rather than a rewrite. The "looking for a backer" line in the README is Symfony boilerplate, not a funding risk signal — but it is worth knowing that no commercial support contract exists.

Support

Community-only, via Symfony's core team. That is strong for a component of this size and weak for anything time-sensitive. The mitigation is internal: a written runbook covering key naming conventions, TTL selection, release discipline, and what to do with a suspected stale permit. Institutional knowledge concentration is the real risk — two engineers should be able to operate it, not one.

Scaling

Throughput ceiling is Redis, not PHP: expect 1–10 ms per acquire/release round trip, so ~2–20 ms of added latency per guarded section. At high request rates that is a material budget, and contention on a hot key serialises throughput regardless of horizontal scaling. Design keys narrowly (render:{tenant_id}, partner_api:{integration_id}) — broad keys turn a concurrency control into a global bottleneck. Align worker counts with permit counts: if Horizon runs 20 workers against 5 permits, 15 workers are parked and the queue backs up invisibly. Watch Redis connection pool exhaustion under Octane.

Failure modes

  • Redis unavailable — decide per call site: fail fast (safe, user-visible) or bypass (fast, correctness hole). Default to fail fast; bypass must be an explicit, logged config.
  • Permit expiry mid-section — two holders, silent data corruption. Mitigate by sizing TTL well above max critical-section duration and alerting on any section exceeding 50% of TTL.
  • Orphaned permits after process kill — self-healing via TTL, but blocks capacity until expiry. Alert on sustained permit saturation.
  • Worker starvation — blocking acquire() in a web request under contention exhausts FPM workers. Prefer short non-blocking acquire with a graceful "busy, retry" response over long blocks.
  • Queue retry storms — a job that times out waiting for a permit re-queues and re-contends. Cap retries and add jitter.
  • Octane/long-lived workers — destructor-based release does not fire per request; explicit finally is mandatory or permits leak until TTL.

Ramp-up

The API is learnable in an afternoon; the semantics are not. Budget a half-day of enablement on the three things engineers get wrong: destructor-based release, TTL longer than the critical section, and key granularity. Add a code-review checklist entry ("every acquire has a finally release and a justified TTL"). The most common failure is not a bug in the package — it is a team adopting a counting semaphore for a problem that needed a binary lock and paying contention costs for no benefit.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log