From magazine topic to project implementation
Relevant service and technical pages for this post
A Windows Service in Delphi often appears unspectacular in day-to-day operation: it runs in the background, processes jobs, writes logs, and talks to databases or REST APIs. Until someone clicks ‚Stop the service‘ — or a patch reboot is due — and the service does not stop cleanly. Then the Services console shows „Stopping…“ for minutes, the service hangs in the Stop Pending status, and in the worst case the process is terminated forcefully. This is exactly where it pays to treat Windows Service in Delphi Graceful Shutdown as an explicit architectural concern: with a clear shutdown signal, defined timeouts, and threads that actually respond.
This article is not about framework internals, but about a practical pattern: TEvent as a stop signal (a kernel-level synchronization object from System.SyncObjs), combined with a Stop-Timeout strategy that accounts for both the Windows Service Control Manager (SCM, i.e. the Windows component that starts/stops services) and your own worker threads. It also covers typical edge cases, debugging approaches, and the question of when the additional logic is really worthwhile.
Windows Service in Delphi Graceful Shutdown in practice
The most common cause is simple: the service has at least one thread stuck in a blocking operation and without a cancellation path. Typical cases:
- Polling loops with Sleep: „while not Terminated do Sleep(1000)“. On stop the signal arrives, but the thread reacts only after up to 1 second (or 30 seconds…).
- Blocking I/O: database calls, HTTP requests, named pipes, filesystem waits — anything that „just waits“ without paying attention to a stop signal.
- Queue consumers without wakeup: a worker waits on a queue, but on stop it is not woken to exit.
- Lock order/deadlocks: cleanup is performed on stop while other threads still hold locks. This often only occurs on the stop path because the ordering there differs from normal operation.
The Windows SCM expects a service to respond promptly to a stop command and to report its status continuously (via SetServiceStatus; Delphi encapsulates that in the service component). If you accept a stop event but do not shut your threads down cleanly, the process remains alive — and Windows eventually decides that it is „taking too long.“ The result is either a hard kill or a service that stays stuck in an unclear limbo.
Basic principle: a stop signal that every worker understands
A Graceful Shutdown only works if you have a signal that:
- can be observed by all relevant threads,
- also takes effect even from blocking wait states,
- is deterministic in the stop path (no “maybe it will eventually come out” hope),
- has a clear timeout strategy.
In Delphi TEvent is a very usable tool for this: an event object that is implemented internally using Windows-handles (comparable to CreateEvent/SetEvent). You can use it as a “Stop requested” signal. Each worker then does not simply wait blindly, but waits “for work or for stop”.
Choosing TEvent correctly: ManualReset vs. AutoReset
For stop signals you typically want Manual Reset: once set, the event remains “signaled” until you reset it. This ensures that any thread that enters a wait phase later will still observe the stop signal. Auto Reset would be risky here because it automatically clears the signal after one waiting thread and other threads might miss the stop signal.
Delphi service lifecycle: where stop actually arrives
An Delphi-Windows- and Linux-services is typically based on TService (VCL/RTL). The SCM sends commands (Start, Stop, Pause, Continue). Delphi then invokes the corresponding events/methods (depending on the template, e.g. OnStart, OnStop, OnExecute).
Important for the architecture:
- OnStop is not the place for long waits without status updates. It is the place where you trigger the shutdown and then wait in a controlled way — with a timeout.
- OnExecute is often a loop. If you do “endless” work there, the loop must react to a stop signal.
- Worker threads (TThread or thread pools) must react to the same stop signal, otherwise the service is logically stopped but not yet physically finished.
Clean pattern: Stop event + join the workers + hard fallback
The practical pattern consists of four steps:
- Request stop: set the stop event, stop accepting new jobs.
- Trigger wakeups: if workers are waiting on queues or sleeping, they must be able to “wake up” (e.g. via an event/queue signal).
- Terminate gracefully: workers exit their loops, close resources (DB connections, files, handles) and report “done”.
- Timeout and fallback: if not everything finishes in time, you must decide: continue waiting (with status updates) or abort/forcefully terminate (depending on the risk).
The core is: no thread should wait exclusively on time (Sleep) or block exclusively on I/O without concurrently considering a stop signal. Instead you use wait functions that consider multiple signals (e.g. “stop event or work event”), or you encapsulate I/O in timeouts plus stop checks.
Thinking about stop timeout correctly: SCM-Timeout vs. own shutdown timeout
This is where most misunderstandings occur in projects. There are two different timeout levels:
- SCM expectation: Windows expects that you regularly report progress while in status SERVICE_STOP_PENDING. Otherwise it appears as if you are hung. Delphi partially takes care of this, but as soon as you yourself block for longer, you need a strategy for how to continue providing status updates (or how to keep your stop phase short).
- Your own shutdown timeout: You define, for example, „We give ourselves 20 seconds to cleanly finish running jobs, then we abort.“ This is an architectural decision: data consistency vs. forced reboot vs. operational requirements.
In practical terms this means: your service should quickly reach a state in which it does not start new work units anymore, and then only waits for running work to finish – but not indefinitely. And this waiting phase should run in small intervals so you can react and, if necessary, log.
How long may the stop last?
There is no magic number that always fits. For many business services a target range of 5–30 seconds is realistic: enough time for „in-flight“ data, but short enough for patch windows. If you regularly need longer, that is often an indication that you process units that are too large at once or that external dependencies (DB/HTTP) run without timeouts.
Implementation with TEvent: a structure that remains stable in operation
A proven structure in the Delphi service looks like this (without delving into framework details):
- A stop event (TEvent, Manual Reset) that is set on stop.
- One or more worker threads that regularly check for stop in their main loop.
- Optionally a work event or a queue that signals work. Workers then wait for „work or stop“.
- A shutdown phase that „joins“ workers (i.e., waits until they have finished), but with a timeout.
The decisive point is not whether you use TThread, omnithreadlibrary or your own pool, but that your workers do not run „blindly“. A worker loop should be structurally like this: wait for event(s) → work in small chunks → check for stop between chunks → release resources cleanly.
Pitfall: Terminate alone is not enough
Many Delphi threads are „terminated“ with Terminate. But that is only a flag. If the thread is currently in a blocking API call, nothing happens at first. That is why a dedicated stop event is so helpful: you can integrate it into wait calls and trigger targeted wakeups.
Pitfall: FreeOnTerminate in the service context
In services you often see FreeOnTerminate := True. That can work, but it makes shutdown harder to control because you then often no longer have a clean reference to wait for thread end and to log error conditions. For controlled stop logic it is usually more stable to own threads explicitly and to wait for and free them deterministically during shutdown.
Blocking operations: how to make them stoppable
The tricky part is not the event itself, but the places where your service blocks. Three typical classes:
1) Replace sleep/polling: wait with a stop event
If you work periodically („check every 10 seconds“), don’t use Sleep(10000); instead wait on an event with a timeout. Then your stop event can end the wait immediately. That reduces stop latency and prevents the impression that the service „is not responding.“
2) Queue consumer: combine work event + stop event
If you have a producer/consumer architecture (e.g. jobs are placed into a queue), you need a signal to wake consumers. Often this is another TEvent (work available). The consumer then waits on two handles: „work“ or „stop.“ When stopping, set the stop event and, if necessary, also the work event so that all consumers are guaranteed to exit the wait.
3) External calls (DB/HTTP): timeouts and cancellation paths
For database access or HTTP calls it determines whether your service stops cleanly. Operational rule: no call without a timeout. A timeout is not a luxury but a prerequisite for controllability. Additionally, you should check for stop between retries/backoff phases. Otherwise you’ll have the classic case: „service does not stop because it is currently doing 10 retries with Sleep.“
With some libraries you can explicitly trigger cancellations (e.g. query cancellation). If that is not possible, at minimum configure timeouts short enough not to exceed the shutdown timeout.
Stop Pending correctly: status, logging and expectation management
When a service is stopping, from an operations perspective it is important to understand where it is hanging. For that you need two things:
- Log markers in the stop path: „stop requested“, „no new jobs“, „waiting for workers“, „worker X terminated“, „shutdown complete.“
- Measurable times: How long does the stop take? Which phase consumes time? Often a monotonic time measure such as GetTickCount64 or TStopwatch is sufficient (monotonic = not distorted by system time changes).
If you write only a single log entry „Stopping…“ in the shutdown path, field debugging remains a guessing game. In service operation, logs are often the only thing you get without interaction.
Which logs are actually helpful in services?
- Service PID, start time, version/build (without excessive overhead).
- Number of active workers, number of in-flight jobs.
- Active external dependencies: „DB call running“, „HTTP request running“, „file flush running“ (only aggregated, not every detail).
- Shutdown timeout reached: which workers are still active?
Field debugging: make it reproducible instead of guessing
Shutdown problems often occur only in production: different load, different latencies, different permissions, different patch windows. A few field-proven levers:
Test the service under control
- Shutdown during active processing (not while idle).
- Shutdown during external failure: DB briefly unavailable, HTTP endpoint slow, fileshare gone.
- Shutdown immediately after start (race conditions: workers still initializing).
Event Viewer and Service Control Manager signals
Windows writes service events, but those are often coarse. It’s better if your service itself writes to a log file or the Windows Event Log. Important: logging should still work in the shutdown path. If you release loggers too early in shutdown or a flush blocks, you lose exactly the crucial traces.
Make hanging threads visible
If you repeatedly see „Stop Timeout“, it’s worth looking at thread states (e.g. via debugger/procdump in a test environment). You will often find a thread in a wait state on a handle that is never signaled, or in a network call without a timeout. The fix is rarely „more sleeping“, but a clean cancellation path.
When is the effort really worthwhile?
A minimalist service that only has a timer and no external dependencies can sometimes „just stop“. But as soon as one of the following applies, a proper graceful shutdown is almost always worth it:
- The service processes jobs with side effects (writing files, DB transactions, API calls).
- There are multiple threads or a pool.
- The service depends on network resources (DB, REST, message broker, fileshares).
- Operations require scheduled maintenance windows (reboots, updates, failover).
The added value is not „elegance“ but operational reliability: fewer hard process terminations, fewer inconsistent intermediate states, fewer manual interventions.
Practical pitfalls: what often goes wrong during shutdown
1) Shutdown is signaled, but new jobs still arrive
If you accept incoming work (e.g. via socket, file-trigger, timer), you must first stop accepting new work in the shutdown path: close listeners, disable timers, stop schedulers. Otherwise you’ll chase the end because new jobs keep starting.
2) Cleanup blocks (flush, close, finalize)
„Just quickly flush everything“ can be dangerous in a service context if the target (network drive, remote log, DB) is currently hanging. Therefore: cleanup yes, but with limited time. If necessary you must decide which data you lose in memory rather than blocking the entire shutdown.
3) Locks and ordering
When stopping, you often access the same data structures as the workers (queues, caches, states). If the Stop thread holds locks and then waits for workers to finish while the workers need the same lock, you have a stop deadlock. Countermeasures: keep lock-hold times short, do not „wait while holding a lock“ in the Stop path, define a clear ordering.
4) Concurrency with duplicate Stop
In practice Stop can be triggered multiple times (e.g. Stop + Shutdown, or Stop happens again). Your Stop path should be idempotent: setting the Stop event is fine, but duplicate join/free logic must be properly protected (e.g. with an atomic flag).
Operational perspective: What Admins and IT leads expect from the service
For operations and administration, what ultimately counts is not how „nice“ the code is, but whether the service:
- reliably terminates on Stop (predictable, without hangs),
- does not produce inconsistent data on Stop (e.g. half-written files, open transactions),
- provides useful logs in case of errors,
- is predictable during maintenance windows and deployments.
This is also why the topic of Stop-Timeout is not just „developer stuff“: it affects patch cycles, recovery times and whether automated deployments are feasible at all.
Concrete guardrails for a robust shutdown design
If you want to standardize this pragmatically, the following guardrails have proven effective:
- A global Stop event, manual reset, created early in the service lifecycle, released late.
- No sleep in worker loops without a stop-capable alternative (wait with timeout).
- All external calls with timeouts (DB, HTTP, fileshares). Choose timeouts so they fit within your shutdown timeout.
- Stop-Timeout as configuration (e.g. in INI/registry), so operations can react without recompiling.
- Staged model: first graceful (let running jobs finish), then optionally „soft abort“ (no new steps), then hard exit as a last resort.
- Good stop logs with phases and timing.
Conclusion: TEvent + Stop-Timeout is not a luxury, but controllability
A hanging Stop is rarely an isolated bug – it is usually an architectural gap: work runs in threads or blocking calls that do not know a common stop signal. With a clear stop event (TEvent, manual reset), stop-capable waits instead of sleep, consistent timeouts for external dependencies and a defined shutdown timeout, you get a service that is predictable in daily operation.
The investment is worthwhile especially if your service runs in production environments with maintenance windows, automated deployments or critical side effects. Then „Graceful Shutdown“ is not cosmetics, but a building block for stable operations and fewer escalations at the next reboot.
If you want to set up your stop path properly or review an existing Delphi service for robust shutdown logic and operational safety, a technical sparring call is often the fastest way to concrete measures: get in touch.
For this topic, Delphi Windows Service and TEvent Delphi are also important. The article places these aspects in an understandable context and shows what matters in everyday operation.
Discuss a project or modernization initiative with Net-Base.
Next step
When the topic becomes an actual project, architecture, existing systems and operations should be considered together from the outset.
We support not only with individual issues, but also when source snippets, legacy topics, or portal ideas are to be turned into a robust enterprise project.
- Current state, target state and technical risks are assessed jointly.
- REST, data access, portals and rollout are not deferred to a later stage as secondary consequences.
- You can see early on which path is economically and operationally viable.