Skip to Main Content
  • Questions
  • Oracle AQ Scaling with PL/SQL Callbacks, DBMS_SCHEDULER Jobs

Breadcrumb

Question and Answer

Connor McDonald

Thanks for the question, Matthew.

Asked: April 19, 2026 - 4:00 pm UTC

Last updated: April 20, 2026 - 11:32 am UTC

Version: 19

You Asked

Hello,

I have an Oracle Advanced Queuing queue and would like to be able to process this queue from inside of the database, as opposed to using an external app server. However, I am concerned about the scalability of internal solutions.

Please assume the following:

1. The queue receives an arbitrary number of messages.
2. Each message results in a PLSQL procedure being called, which can take an arbitrary amount of time.
3. You want to limit the number of messages that can be processed at once to some value N.

---

Solution #1: Run N permanent DBMS_SCHEDULER jobs that loop and call DBMS_AQ.DEQUEUE with WAIT_FOREVER. This is good because you can easily cap how many jobs you want to have processing this queue by adjusting the N number of permanent DBMS_SCHEDULER jobs. This is bad because all of these permanent jobs will reduce the available JOB_QUEUE_PROCESSES. It is fine if you only need a handful of jobs to process your queue, but as you scale the number of jobs up, eventually you will degrade the other unrelated jobs that need to run in the system.

Does calling DBMS_LOCK.SLEEP or DBMS_AQ.DEQUEUE inside a DBMS_SCHEDULER job free up a JOB_QUEUE_PROCESSES slot while the job is sleeping? My guess is no.

---

Solution #2: Use a PL/SQL callback, and in the callback, create a one-time DBMS_SCHEDULER job per message, and use a common resource constraint, such that only N scheduler jobs can run at once. For example, if you set a cap of 128 jobs in your resource constraint, and you receive 1000 messages, the PL/SQL callback will create 1000 jobs, but only 128 jobs will be running at once, and the rest will be blocked.

The downside here is that you have to create a whole dbms_scheduler job to process a message. This will increase the time between receiving a message and starting to process it, and just seems like an overall heavy solution. Lightweight jobs won't help because the resource constraints don't work for lightweight jobs.

In fact, you might as well not use AQ at all if you go down this route. Instead of writing messages to a queue, which later calls DBMS_SCEDHULER.CREATE_JOB, you could simply call DBMS_SCHEDULER.CREATE_JOB directly with a resource constraint.

---

Solution #3: Use an external app server. Run N threads, where each thread grabs a connection from a connection pool, loops and calls DBMS_AQ.DEQUEUE with WAIT_FOREVER. This is the best approach because you can set N to cap the number of connections processing messages in parallel easily, and you do not have to block up and slots in JOB_QUEUE_PROCESSES.

However, this has downsides. Your app server often has much more downtime than your database due to releases, network partitions, and many other various issues. If your session which is executing a long running PL/SQL procedure is terminated, you cannot assume whether the PL/SQL procedure on the server will complete or be stopped. While this is also true for DBMS_SCHEDULER Jobs that end up getting killed when the database has unexpected downtime, the frequency of downtime is much less compared to an app server.


---

Event based jobs won't work, because you have to use parallel_instances to true (or else messages will be skipped), which would then force the use of lightweight jobs, which would then prevent the use of resource constraints. Even if you could use normal jobs or set resource constraints, you might as well not use AQ and just schedule the jobs yourself with a resource constraint.

---

Is there any solution that can be implemented internally to the database, which does not permanently block JOB_QUEUE_PROCESSES slots even when there is no messages in the queue, and which doesn't rely on excessive one time job creation via DBMS_SCHEDULER?

with LiveSQL Test Case:
https://freesql.com/?compressed_code=H4sIAAAAAAAACp1YbW%252BbOhT%252BXqn%252FwR%252BulFZK03VXurpq1TsR4nXZCKSGdOsni4CbsBGcgVlXaT%252F%252BHhtMDKGvUT4k5rz5%252BJznPObkBPlMoHKLrktWssODwwObYCvAyCOI4Llj2RgFt3OMZrf0eoEXmKp%252Flo%252B88WdsB%252Bjo8ADB545zdGMR%252B5NF3h%252Bdvf%252F3%252BPDg%252BEKam4xnPrWurclsVFnWZqyxg2vln9I3FeEyZejyPzTQvgZD8%252Fk2fEh5GFPxsG2LqZAGzzhsucrCzVOeeiLpse4HFgleZPwtytb1X7TZYBMfy5SYlPhoOT6ul2PWLAdkgbW%252FEzhdnpYi4Rk6Q6TMkIu2LN%252BEGcsEUtH49ic8WTiYoO98WSCxDgVKOd%252BiMItRFKYpqoMeTbCKBd0nYo2%252BWtOAfvQIvsFkVLsK1kmB4LsMY7RkUVgWDN2zncP0AS1THv0AJwx99sb13ubEs7HvYx8VKRdFZR9EQNuMVoY3Un6quB2M5%252Bi0ic6Z%252BgF2UcxZgTIuUM5SFoKJylBtec0y5RxsyUCLlLFtkq2GKE9Wa%252FGhv%252FpVfJMFwWjzQOtEHx5M%252FSrzqV6ifCvzXHTzRb15MPVcnwYXWmPDiiJcQUHnHPYnErZTmkEmrCuVlDkmwRT36SUxItbXo7N%252FjptHujf48rsMsy5jaBSQGOOrqVsJOp43r371xj66DxOBzi%252BbcMxjvnhKUf%252Ff8JiZBgieeTdYFaNW7uTnaPek0wX609en%252BtNNfiW%252BF19Hq%252BcAlNb%252BekexTrO5pBSN9HddFaukJY%252FarpJ4J3%252FcyhP7zaJSMBrmy0TkYf5At2nxMz1qeRsB7rbVbG82m%252Bqawe5EnTn8lT93BQwLp3Xb2jkLBet0WgUEVbv2teqJ7PJRq7agTFCCpi4gzWiEYAR0qq0NNxqbwXinAsD9PoTSrx75ggkdoD9%252FUDLc12iGwtzxrx06djz7S7dYpFwYKTSUkip2MycyXRddpUKEuaCxTBEo%252Bbd%252BMIUmDazZvCOYsy0kkiaZYPmvMFUuPhJ8fTmbuosAO7cXkJwAkxvLufz7XdcNy%252BTMiXcIbhbF%252FlHq82tj%252FHu0ALAL0dw5hSQo%252BF6G0Y%252BhAvOkwr7dYlQdfIh4xk5EAhnfnwiyLHS7VGZK5SHimw04zFnByzwCq9BkUKSw%252BSEqymhd1Q%252FPAPNdVERrFpcpWFKlFYWgCLNICURMjw%252BXC1apCTlICr2pRAJ6VjD4XqAHXqKNBGsUAp4zGE8S7GVM1rUK73tZiN3OHtkQ%252BwV13q7e%252Fuok2PcWxDZBSm%252B5p0a1tHm2ZZYIhS7QEMYyVJUo1foAu9A5NqY2DImAWFAj%252FqAS3DV2f3TQjlfEmhnBAXCt8nDTE1sta4amhZvW8QNA%252BgltBl6fsNE%252F%252BBu2obCpRcZTCJzcUtV7plZWbpYsp%252FwOUGxVbiDrVS4MEaPwFaPZbb1n4xP8ceo2mwHPV4sZdoO3ZkAHRbe8SPS%252BzvoEGnNzGIcenfZaaRKpibApFbO7sEwFBWioiJq7cJwnN%252BvjoCkpozqMzcIAYJF42V7fXLdPxohdg8arE380lp0dA7%252BeoVvVENeQVbsBrBHst0By1gAPqoPO2SrJ7rhaBZQeSf5M8BWdQns1LLmIcvnDEJlg3ybTOVR%252BLdSMeBDS59h%252BlMpH7mI2xgSItkkF9wma5ELK7QiIAKw3hK2Zcq1LkxTvn3mm8TanexOLezODe5y9vYK5vZ61vYmxvY6tvZSpPYfKbUZjspndqfcA64ua%252BEVkpIuoJlSVgtMYctXmGU8AkCR%252FGmYpUJfWkT%252B7udfhZwONexT3bRi0C6mFPfvAUl3Ose1YBOv%252BADyhaQJkYg9NqLxrXuyzB9UsV%252FIeSlp8Qdupj2zfVocAd2U6j5sd6hJ99HKkJYttGKmE6DBdC6pmDmgLf3p0dFoq6%252BrWcX562tSlbTnO2LK%252FfJiTy7M%252Bnw1AS%252F1P%252BBuAK6Dh4N27wXFb2Ph7PGynLOIlXEU6A8gYHCbt%252Ffu84r0Z3JmAfGdAvsPtFhXAw1k%252Bqt97iDWQwrgYyrcAOUMsVCRVriHoP6ClktRmGav4zV3ON%252B2VLefpUL0aKZp3I3uX%252FcdfjtzC8DWiAkDLGZBj4MGFouV3PE35fZKtJE9voj2X2kadtS5w7WwEYCTm91mRAH4n9QUuuZOMWTlOkyhUG6kDABEpPqzfkGhmLun0kklUigD3WDw6OcGe%252FT%252BspNaEJxQAAA%253D%253D&code_language=PL_SQL&db_version=19&code_format=false

and Connor said...

A couple of options I've used successfully (on customer sites with high throughput, 1000s txns per second).

(Variant on) Solution 1:
==================
- as you have described but jobs dont run forever. The method I opted for is (pseudo code)

loop
- wait on queue for 20 seconds
- if messages found, process them
- if messages not found, log a message to a log table (so we know things are running as we expect)
- when total elapsed job time = "n", exit the loop (eg n = 3 mins)
end loop;

So a now a job never runs more than 3 mins, and I've got it scheduled (say) every 2 mins, so the moment it finishes it will commence again. That way you get useful scheduler logs about frequency, plus you can collect data about messages per second/minute etc. We also (due to our requirements) could use array dequeue, so we'd listen on a single message, *deliberately* wait 5 seconds, and then batch dequeue whatever was there. More efficient by trading a little latency. (There was also priority messages etc but that's getting too much into the weeds).

You don't want too many concurrent jobs anyway, because you would not want to go close to server core count, so its not like you want 100s of these things.

(Variant on) solution 3
==================
I've also done this at a customer. We're running a program *external* to the job scheduler, but *internal* the database server, ie, just a series of Pro*C jobs running on the database server itself. It requires more monitoring (failures/restarts etc etc) but you eliminate the SPOF because if the database server does down, you've got bigger issues to deal with anyway

Rating

  (1 rating)

Comments

Matthew Moisen, April 20, 2026 - 5:07 pm UTC

Comment is awaiting review by moderator, and will be posted once reviewed

More to Explore

PL/SQL demos

Check out more PL/SQL tutorials on our LiveSQL tool.

PL/SQL docs

PL/SQL reference manual from the Oracle documentation library